Skip to content

beginWork(FiberNode更新入口)

这个在React的源码中是一个单独的文件 react-reconciler/src/ReactFiberBeginWork.new.js

这个阶段的主要特点是:

  1. 产生 current (没有current的说明是初次渲染,然后在beginWork的流程通过workInProgree生成FiberNode,并存在alternate上,作为下次的current )
  2. 根据 lanes 和 childLanes 生成FiberNode

其主要作用是:根据组件类型的不同生成对应的FiberNode对象,并在update的过程判断原FiberNode是否可以复用

其主要分为两个部分

  1. 根据各个条件判断是否可以复用(didReceiveUpdate )
  2. 根据 workInProgress.tag 调用对应组件类型的处理方法生成FiberNode

栗子

js
   const Comp211 = props => {
        return <div>this is Comp211</div>
      }
      const Comp221 = props => {
        const [num, setNumber] = React.useState(1)
        const handleClickBtn = () => {
          setNumber(preNumber => preNumber + 1)
        }
        return (
          <div>
            this is Comp221
            <div>
              <button onClick={handleClickBtn}>点击更新 - {num}</button>
            </div>
            <Comp2211 /> 
          </div>
        )
      }
      const Comp2211 = props => {
        return <div>this is Comp2211</div>
      }

      const Comp21 = props => {
        return (
          <div>
            this is Comp21
            <Comp211 />
          </div>
        )
      }
      const Comp22 = props => {
        return (
          <div>
            this is Comp22
            <Comp221 />
          </div>
        )
      }
      const Comp23 = props => {
        return <div>this is Comp23</div>
      }

      const App = props => {
        return (
          <div>
            <Comp21 />
            <Comp22 />
            <Comp23 />
          </div>
        )
      }

image

如上述栗子,我们在组件Comp221点击了一个点击事件,从而触发了一个Update,那么在beginWork的之前 performUnitOfWork中,会在const root = markUpdateLaneFromFiberToRoot(fiber, lane) 的时候,1. 通过Comp221组件的return找到跟FiberRoot; 2. 在向上寻找的过程中会标记Comp221组件的 lanes 和 祖先组件的 childLanes。结果如下图(注意: 在任务调度的时候 可能会有多个更新,那么也就可能存在多个更新的线路)

image

FiberNode 复用和强制更新

我们看一下判断条件

js
  // ---------------     判断是否需要强制更新  ---------------------
  // current !== null 就是组件不是初次渲染
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
      // 如果组件的 1. props 改变了 2. context 改变了 3. 组件类型不同
      // 那么组件就不能复用, 需要强制更新了
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      // Force a re-render if the implementation changed due to hot reload:
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // If props or context changed, mark the fiber as having performed work.
      // This may be unset if the props are determined to be equal later (memo).
      didReceiveUpdate = true;
      // 判断组件上的优先级是否包含当前渲染的优先级
      // 如某一个子组件触发了 更新如 setState 那么这个组件的 Lanes 就会包含这个优先级 即尾位为1
      // 当前渲染的通道为 renderLanes  如 000000000000xxx0001
      // 这时候就是包含,跳过
      // 对于上级节点或者其他节点等 lanes 可能为 0000000 , 这时候不包含,那说明这个节点不需要重新render
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false;
      switch (workInProgress.tag) { ... }
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    } else {
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        didReceiveUpdate = true;
      } else {
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
  }

从上面找到组件复用的条件就只有一点点 return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);可见对于FiberNode的复用的条件还是很多的

current

在ReactDOM.render流程中,会在render的renderRootSync下prepareFreshStack初始化跟的workInProgress 并将原FiberNode指向workInProgress.alternal,到performUnitOfWork的时候会将这个作为current。

那么看见 current就是代表原来的 FiberNode对象

上文 if(current === null ){ didReceiveUpdate = false; } 说明此组件是初始渲染,肯定走创建流程

可见对于beginWork其又可以分为主要的两个阶段:向下遍历 和 向上回溯 的过程。

bailoutOnAlreadyFinishedWork( current , workInProgress , renderLanes )( 组件复用方法 )

我们先简单看一下组件复用的代码

js
/**
 *  跳过当前节点的render
 *   - 对于 childLanes 包含的 renderLanes , 就会进行 cloneChildFibers, 进行子节点的 beginWork
 *   - 对于 childLanes 都不包含 当前renderLanes ,直接跳过
 * @param {*} current
 * @param {*} workInProgress
 * @param {*} renderLanes
 * @returns
 */
function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {

  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    return null;
  } else {
    // This fiber doesn't have work, but its subtree does. Clone the child
    // fibers and continue.
    // 虽然这个节点可以复用,但是其子节点不一定是可以复用的,所以需要clone子节点 并向下继续render
    //   主要作用于  有更新组件这条线上面的组件,他们本身可以复用,但是子节点不一定可以复用了
    cloneChildFibers(current, workInProgress);
    return workInProgress.child;
  }
}

可见在**bailoutOnAlreadyFinishedWork( current , workInProgress , renderLanes )​的逻辑是非常简单的,那么结合上述例子和 beginWork中进入​bailoutOnAlreadyFinishedWork​的判断条件​我们就可以大体了解 FiberNode 复用的条件了 **

首先我们不考虑多更新线路,多优先级的情况下,我们可以将上述FiberNode树分为以下类型

  • 不是Update更新祖先线路上的其他分支节点Node(如Comp21、Comp23)
  • Update更新祖先节点Node(其childLanes === 0b000001)(如App等)
  • Update节点本身(Comp22)(其lanes === 0b00001)
  • Update节点子孙节点

下面就是围绕这四种类型的节点去区分 重新生成节点及其子孙全都复用节点本身复用(子孙继续处理)

  1. oldProps !== newProps || hasLegacyContextChanged() 组件的** Props不同 **或者 **上下文Context改变 **了那么肯定不能复用,直接didReceiveUpdate 强制更新

  2. !includesSomeLane(renderLanes, updateLanes) 优先级不同也直接跳过(updateLanes === FiberNode.lanes)

    • 在一次任务调度过程中可能存在多个优先级的更新,那么对于其他优先级的更新功能肯定不做处理了

    • 除UpdateNode本身其他的节点也会进入到 bailoutOnAlreadyFinishedWork( current , workInProgress , renderLanes )的流程

      但是具体是 节点及其子孙全都复用、节点本身复用,需要再判断

  3. !includesSomeLane(renderLanes, workInProgress.childLanes)

    这就是判断 节点及其子孙全都复用节点本身复用

    在**bailoutOnAlreadyFinishedWork**中通过判断了 childLanes ,如上述例子

    • 如果不在当前更新的祖先节点上,那么就可以直接 节点及其子孙全都复用
    • 如果是在当前更新的祖先节点上,那么节点本身可以复用,其子孙节点能不能复用需要再次判断(加入到workInProgress进入循环)。即节点本身复用
  4. (current.flags & ForceUpdateForLegacySuspense) !== NoFlags

    TODO: 待分析

scheduleUpdateOnFiber 的 markUpdateLaneFromFiberToRoot(fiber, lane)方法中对于触发Update的组件会将其FiberNode.lanes上标记触发更新的优先级。

如果我们只判断组件的 props 和 上下文context 没有更新就直接复用FiberNode,那么对于触发更新本身就不更新了(错误),所以我们还需要添加一个如果存在当前优先级的更新标记,那么也需要重新创建

** FiberNode复用的过程( cloneChildFibers(current, workInProgress) )**

从上述可以看出进入到这个过程的主要是 UpdateNode的祖先节点(Node)

js

export function cloneChildFibers(
  current: Fiber | null,
  workInProgress: Fiber,
): void {
  invariant(
    current === null || workInProgress.child === current.child,
    'Resuming work not yet implemented.',
  );
  // 没有子节点的情况。 返回null,从而进入 completeWork 过程
  if (workInProgress.child === null) {
    return;
  }

  let currentChild = workInProgress.child;
  // 创建子节点的workInProgress
  let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);

  // child 与 return 的相互绑定
  workInProgress.child = newChild;
  newChild.return = workInProgress;

  // 处理兄弟节点
  while (currentChild.sibling !== null) {
    currentChild = currentChild.sibling;
    newChild = newChild.sibling = createWorkInProgress(
      currentChild,
      currentChild.pendingProps,
    );
    newChild.return = workInProgress;
  }
  newChild.sibling = null;
}

可以看出 cloneChildFibers 也没有啥Diff算法的,就是创建子节点及其兄弟的 workInProgress,并与当前workInProgress进行child和return的相互绑定

总结

从上述流程总结得出 render的beginWork的过程,其主要是对FiberNode的新建、复用过程。

  1. 通过 current 和 workInProgress 去维护两条FiberNode树

    根据深度优先的规则,通过修改当前workInProgress 的指向,不断形成新的FiberNode树

  2. 通过在const root = markUpdateLaneFromFiberToRoot(fiber, lane) 的过程标记的 lanes 和 childLanes,从而区分1. 当前UpdateNode 2. 当前UpdateNode祖先节点 3. 当前UpdateNode祖先的其他分支节点(当然包含子孙节点)

  3. 在 beginWork的时候通过 props、context、lanes、childLanes来区分节点是否可以复用

  4. 对于可以复用的节点,通过是否是分支节点或者更新的子孙节点(props和context没变化的),从而直接复用整个节点树,避免了其子孙节点的beginWork和completeWork的过程