likes
comments
collection
share

beginWork 核心流程

作者站长头像
站长
· 阅读数 27

问题

1、react更新逻辑是从roo开始的,那么是如何找到发生更新的组件呢?

2、fiber进行beginWork一定会render吗?beiginWork和render的关系是怎么样的呢

3、beginwork的更新流程为什么是从组件开始的?

4、childLans的作用是什么?

state改变是组件更新的唯一条件

在当前react版本,视图的更新来源于组件内部 state 的改变。如果想要更新视图,那么只有以下操作方式

  • 组件自身的state发生改变 

    • 函数组件:useState或者 useReducer
    • 类组件:setState或者forceUpdate
    • 父组件更新导致的更新
    • Provider祖先组件的state发生改变,当前组件使用了useContext或者消费了context,

无论是上述哪种方式更新,本质上都是state的改变

state改变是在组件对应的 fiber上的,在react中存在多种多样的fiber类型,但是在react体系中只有 函数式组件 和类组件 才能进行改变state,从而影响调和,比如hostComponet(html原生标签)是无法进行独立的更新的,只能依赖于父组件的state进行更新,但是在调和阶段,他也会作为一个任务但愿进入到workLoop

fiber是调和过程中的最小单元,每一个需要调和的 fiber 都会进入 workLoop 中

组件是最小的更新单元,React 的更新源于数据层 state 的变化

beiginWork是fiber更新的入口

我们一起深入来研究一下组件类型的fiber,类组件/函数式组件在 render 阶段的一个重要作用就是产生新的 子fiber节点 ,也就是我们常说的 rerender。接下来才能使用深度优先遍历算法遍历fiber 从而改变视图。每一个需要调和的 fiber 都要经历一个过程叫做 beginWork ,在 beginWork 流程中将执行上述各种 fiber 的更新函数

组件类型 fiber 进入到 workLoop 中,那么一定会 rerender 吗,答案是否定的

我们一起来看看下列几种情况


interface Child1Props {
  count?: number;
}

const Child: FC<Child1Props> = ({count}) => {
  return (
    <>
    <span>我是子组件1</span>
    <span>来自父组件的数量为{count}</span>
    </>
    );
}
interface Child2Props {}

const Child: FC<Child2Props> = () => {
   const [count , setCount] = useState<number>(0);
  
    const onClick = () => {
      setCount(count++);
    }
  
  return (
    <>
     <span>子组件2自己内部的数量为{count}</span>
     <button onClick={onClick}>子组件2按钮</button>
    </>)
}
interface ParentProps {}

const Parent: FC<ParentProps>(){
    const [count , setCount] = useState<number>(0);
  
    const onClick = () => {
      setCount(count++);
    }
  
    return
      <>
        <p>父组件的数量为 {count} </p>
        <Child1 />
        <Child2 />
        <button onClick={onClick} >父组件按钮</button>
    </>)
}

场景一:如上 demo 中,当点击 Child2 的 子组件2按钮 的时候,Child2 会渲染,那么 Child2自然会进入到 beginWork 流程中,那么疑问来了

  • 问题一:父组件 Parent 没有更新,会 rerender 吗?那么有会进入 beginWork 流程吗 ?
  • 问题二:Child1会进入 beginWork流程吗 ?
  • 问题三:如果 Parent 会 beiginWork,那么 React 从 Root fiber 开始调和的时候,是如何找到发生更新的组件呢?

场景二:在如上 demo 中,当点击 Parent 中的父组件按钮的时候:

  • 问题一:Parent因为本身的state 改变会更新,那么 Child1 和 Child2 为什么会跟着更新。

接下来我们开始以一次更新开始,分析调和过程中 beginWork 流程,在开始之前简单的了解一下lane

  • lane : 更新优先级。(在一次更新任务中,将赋予给更新的 fiber 的一个更新优先级 lane。)
  • childLanes:子fiber 中更新优先级。(如果当前 fiber 的 child 中有高优先级任务,那么当前 fiber 的 childLanes 等于当前优先级)。

住这两个概念对于下面流程分析很有帮助。接下来带着上面的四个问题,开始往下分析

我们思考一下调和算法是从rootfiber开始的,也就是从根root开始的,那么我们在调和过程中如何快速找到呢,如果你作为react的设计者应该如何去设计呢,当然答案很简单,如果我们在调用更新state的时候,给当钱组件一个标记,通过fiber的return指针反向去标记父节点直到组件根节点,然后在调和阶段只要找到和当前标记相同的的最后一个的节点,那么该节点即为state发生改变的组件,对应着react源码中的markUpdateLaneFromFiberToRoot ,核心代码如下

function markUpdateLaneFromFiberToRoot(sourceFiber,lane){
    /* 更新当前 fiber 上 */
    sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
    /* 更新缓存树上的 lanes */
    let alternate = sourceFiber.alternate;
    if (alternate !== null) alternate.lanes = mergeLanes(alternate.lanes, lane);
    /* 当前更新的 fiber */
    let node = sourceFiber;
    /* 找到返回父级 */
    let parent = sourceFiber.return;
    while(parent !== null){
        /* 更新 childLanes 字段 */
        parent.childLanes = mergeLanes(parent.childLanes, lane);
        if (alternate !== null) {  alternate.childLanes = mergeLanes(alternate.childLanes, lane); }
        /* 根据return指针向上遍历更新lane */
        node = parent;
        parent = parent.return;
    }
}

markUpdateLaneFromFiberToRoot 做的事很重要。

  • 首先会更新当前 fiber 上的更新优先级由于fiber 架构采用双缓冲树,所有还要更新当前 fiber 的缓冲树 alternate 上的优先级
  • 然后会递归向上把父级连上的 childLanes 都更新,更新成当前的任务优先级
  • 既然要从root开始调和,又不想调和整颗树,那么如何找到发生更新的组件呢?其实和我们想的是一样的,react采用childLanes作为标记,如果组件发生了更新,通过return向上递归标记改变父fiber的 childLanes,接下来从 Root Fiber 向下调和的时候,发现 childLanes 等于当前更新优先级,那么说明它的 child 链上有新的更新任务,会向下调和直到找到最后一个fiber的优先级等于当前更新优先级的fiebr

我们简单的了解一下fiber树的数据结构具体长什么样子,大致如图所示

beginWork 核心流程

  • return指针:指向了当前fiber父节点
  • child指针:父fiber指向的第一个子fiber
  • sibling: 子fiber指向的下一个子fiber

在简单的了解fiber的简单的数据结构以后我们来探讨一下,整个调和过程是怎么样的呢

组件更新前如图所示

beginWork 核心流程

组件发生了更新如图所示进行标记并改变childLanes

beginWork 核心流程

从根节点开始向下调和,直到找到优先级等于当前更新优先级的fiber

beginWork 核心流程

  • 第一阶段是组件发生更新,那么产生一个更新优先级 updateLane ,并且当前的组件lane赋值为updateLane
  • 第二阶段通过return指针向上标记更新父fiber的 childLanes为updateLane
  • 第三阶段是向下调和过程,如果父fiber被调和,那么父fiber的前几个兄弟节点一定会被调和阶段,并且父级的第一级子链的fiber都会进入调和流程

beginWork 流程

我们从调和阶段其实发现了一个有趣的现象,react首先先沿着child找到第一个子fiber,直到找到最后一个不存在子fiber的节点,然后沿着当前fiber的sbling进行寻找,直到当前节点不存在sbling,然后然后到父节点继续沿着父节点的sibling进行搜索,其实这个过程在recat被分为beginWork 和competeWork

beginwork阶段主要做的是向下调和的过程,既沿着child指针向下遍历以及沿着sbling指针向右遍历

但是只有发生state改变的组件才会rerender以及当前组件的后代组件会发生render,为了更好的理解这句话,我们作了如图所示

假设有一个组件 fiber 链:root -->child--> A组件 -->child--> B组件 --child--> C组件,如图所示

beginWork 核心流程

以B组件为例,发生更新的情况一共有4种情况

  • 更新 A 的state,只会标记A和root,然后 root和A 会被调和,在没有作任何优化的前提下,B,C组件不进入调和,一定会发生render
  • 更新 B 的state, B,A和root 会被标记,然后 root和A 会被调和,但是不会 rerender;组件 B 是既会进入调和,也会 rerender;组件 C 受到父组件 B 的影响,不会进入调和,但是会进入rerender
  • 更新 C的State,那么 A,B 会进入调和流程,组件 C 既会进入调和,也会 rerender
  • 更新 root 的state,那么 root会被标记,并进入调和阶段,A 受到root 的影响,不会进入调和,但是会进入rerender,(这也是为什么context发生改变,会导致所有后代fiber进行rerender的原因)
  • renderLanes不等于B的childLanes,不会进行B的调和
/**
 * @param {*} current         current 树 fiber 
 * @param {*} workInProgress  workInProgress 树 fiber 
 * @param {*} renderLanes     当前的 render 优先级
 * @returns 
 */
function beginWork(current,workInProgress,renderLanes){
    /* -------------------第一部分-------------------- */
    if(current !== null){ 
        /* 更新流程 */
        /* current 树上上一次渲染后的 props */
        const oldProps = current.memoizedProps;
        /* workInProgress 树上这一次更新的 props  */
        const newProps = workInProgress.pendingProps;
        
        if(oldProps !== newProps ||  hasLegacyContextChanged()){
          didReceiveUpdate = true;
        }else{
          /* props 和 context 没有发生变化,检查是否更新来自自身或者 context 改变 */
          const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current,renderLanes)
          if(!hasScheduledUpdateOrContext){
              didReceiveUpdate = false;
              return  attemptEarlyBailoutIfNoScheduledUpdate(current,workInProgress,renderLanes)
          }
          /* 这里省略了一些判断逻辑 */
          didReceiveUpdate = false;
        }

    }else{
      didReceiveUpdate = false
    }
  
    /*  组件会被调和,进入更新阶段 */
    switch(workInProgress.tag){
        /* 函数组件的情况 */
        case FunctionComponent: {
           return updateFunctionComponent( current, workInProgress, Component, resolvedProps, renderLanes )
        }
        /* 类组件的情况 */
        case ClassComponent:{
          return updateClassComponent(current,workInProgress,Component,resolvedProps,renderLanes)
        }
        /* 元素类型 <div>, <span>  */
        case HostComponent:{
          return updateHostComponent(current, workInProgress, renderLanes)
        }
        /* 其他 fiber 情况 */ 
    }
}

从源码中我们可以看到beiginwork主要分为2个阶段

  • 第一阶段
  • didReceiveUpdate :标记当前更新是否来源于父级的更新

  • 首先通过 current !== null 来判断当前 fiber 是否创建过,如果 current 为 null,则进入mounted阶段, 否则进入update阶段

  • oldProps === newProps

    • 判断当前组件更新前后的props是否相等
    • 通过 useMemo或者meno 等方式缓存了 React element 元素
    • 更新发生在当前组件本身,但是 B 组件的 props 并没有发生变化

反之如果两者不想等,证明父级 fiber 重新 rerender 导致了 props 改变,此时 didReceiveUpdate = true ,那么第一阶段完成,进入到第二阶段

  • 第二阶段更新 fiber

    • 函数组件调用 updateFunctionComponent然后进行 rerender
    • 类组件调用 updateClassComponent然后进行 rerender

beiginWork流程图

beginWork 核心流程

总结

  • 组件更新和调和过程。rerender 一定会调和,但是调和并不一定 rerender。。
  • 标记是从state发生的改变沿着fiber的return指针向上标记
  • 调和是从根接节点向下调和的,直到找到renderLanes等于lans的节点停止调和
  • 进入beginWrok阶段,判断当前组件更新的原因是什么原因引起的
  • 执行渲染fiber
转载自:https://juejin.cn/post/7372863316911702057
评论
请登录