Reconciler阶段概览

Reconciler阶段就是调和的过程,即:

  • 采用深度优先遍历算法,生成fiber结构,构建对应的workInProgress树,
  • 找到diff的
  • 根据优先级调度任务
  • 根据节点态生成一个Effect List

通过前面章节我们知道,在render方法中调用updateContainer,沿着这条调用链接
updateContainer -> performSyncWorkOnRoot -> renderRootSync -> workLoopSync ->performUnitOfWork

那么render阶段开始于performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新。

  1. // performSyncWorkOnRoot会调用该方法
  2. function workLoopSync() {
  3. while (workInProgress !== null) {
  4. performUnitOfWork(workInProgress);
  5. }
  6. }
  7. // performConcurrentWorkOnRoot会调用该方法
  8. function workLoopConcurrent() {
  9. while (workInProgress !== null && !shouldYield()) {
  10. performUnitOfWork(workInProgress);
  11. }
  12. }

可以看到,他们唯一的区别是是否调用shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历。
workInProgress代表当前已创建的workInProgress fiber。
performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树。

我们知道Fiber Reconciler是从Stack Reconciler重构而来,通过遍历的方式实现可中断的递归,所以performUnitOfWork的工作可以分为两部分:“递”和“归”。

递阶段-beginWork
首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork方法(opens new window)
该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。
当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。

归阶段-completeWork
在“归”阶段会调用completeWork(opens new window)处理Fiber节点。
当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。
如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。
“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了。

demo

function App() {
  return (
    <div className="App">
    <h3>
      <p>hello React source code~</p>
    </h3>
      <span>Joy Guan</span>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

对应fiber树
image.png
分别在源码中的递和归阶段打log
image.png
其中的tag 定义见 tag定义
tag为3说明是HostRoot,即HostRootFiber
tag为2是IndeterminateComponent 决定是函数式组件还是类组件
tag5为5是HostComponent

由打印结果可以清晰看到 递-归过程。

递-beginWork

beginWork 的工作是传入 当前Fiber节点,创建对应的 子Fiber节点。

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...省略函数体
}

其中传参:

  • current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
  • workInProgress:当前组件对应的Fiber节点
  • renderLanes:优先级相关,在讲解Scheduler时再讲解

rootFiber以外, 组件mount时,由于是首次渲染,是不存在当前组件对应的Fiber节点在上一次更新时的Fiber节点,即mount时current === null。

组件update时,由于之前已经mount过,所以current !== null。

所以我们可以通过current === null ?来区分组件是处于mount还是update。

基于此原因,beginWork的工作可以分为两部分:

  • update时:如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child。
  • mount时:除fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {

  // current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
  // workInProgress:当前组件对应的Fiber节点
  // renderLanes:优先级相关,在讲解Scheduler时再讲解

  //更新阶段
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged()
    ) {
      // props和context没有变化
      didReceiveUpdate = true;
    } else {

      // 检查是否存在正在更新的Context
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
        current,
        renderLanes,
      );

      if (
        !hasScheduledUpdateOrContext &&
        (workInProgress.flags & DidCapture) === NoFlags
      ) {
        // didReceiveUpdate === false(即可以直接复用前一次更新的子Fiber,不需要新建子Fiber)
        didReceiveUpdate = false;
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );
      }
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        didReceiveUpdate = true;
      } else {
        // An update was scheduled on this fiber, but there are no new props
        // nor legacy context. Set this to false. If an update queue or context
        // consumer produces a changed value, it will set this to true. Otherwise,
        // the component will assume the children have not changed and bail out.
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
  }

  workInProgress.lanes = NoLanes;

  // mount时:根据tag不同,创建不同的子Fiber节点
  switch (workInProgress.tag) {
   case IndeterminateComponent: 
    // ...省略
   case LazyComponent: 
    // ...省略
   case HostRoot:
    // ...省略
   case HostComponent:
    // ...省略
   case HostText:
    // ...省略
   case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case MemoComponent: {
      const type = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      // Resolve outer props first, then resolve inner props.
      let resolvedProps = resolveDefaultProps(type, unresolvedProps);
      resolvedProps = resolveDefaultProps(type.type, resolvedProps);
      return updateMemoComponent(
        current,
        workInProgress,
        type,
        resolvedProps,
        renderLanes,
      );
    }
  }
   // ...省略其他类型

}

可以看到,不同类型组件对应不同的处理,我们常见的classComponent、fucntionComponent、memoCompoent分别调用了updateComonent/updateFunctionComponent/upddateMemoComponnet
最终会进入reconcileChildren(opens new window)方法。

reconcilerChildren

reconcilerChildren做了什么呢?

  • 对于mount组件,创建fiber节点
  • 对于update组件,对比当前组件和改组件在上次更新时对应的fiber节点 做比较(diff),将diff的结果生成新的fiber节点 ```javascript export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderLanes: Lanes, ) {

    if (current === null) { //针对mount组件 workInProgress.child = mountChildFibers(

    workInProgress,
    null,
    nextChildren,
    renderLanes,
    

    ); } else { //针对update组件 workInProgress.child = reconcileChildFibers(

    workInProgress,
    current.child,
    nextChildren,
    renderLanes,
    

    ); } }

mount和update分别调用mountChildFibers和reconcilerChildFIbers,最终会进入diff阶段(ChildReconciler),diff阶段分析详见 [diff算法详解](https://www.yuque.com/guanguan-ky3w9/cf85a3/rqwl65)

不论是mount还是update,最终他会生成新的子Fiber节点并赋值给workInProgress.child,作为本次beginWork返回值,并作为下次performUnitOfWork执行时workInProgress的传参。

> 值得一提的是,mountChildFibers与reconcileChildFibers这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers会为生成的Fiber节点带上effectTag属性,而mountChildFibers不会。


effectTag类型有
```javascript
// DOM需要插入到页面中
export const Placement = /*                */ 0b00000000000010;
// DOM需要更新
export const Update = /*                   */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /*       */ 0b00000000000110;
// DOM需要删除
export const Deletion = /*                 */ 0b00000000001000;

我们知道 render阶段是在内存中进行的,render结束后要通知 Renderer 根据effectTag执行对应的DOM操作,

那么,如果要通知Renderer将fiber节点 对应的DOM节点 插入到页面中,需要满足:

  1. fiber.stateNode存在,即Fiber节点中保存了对应的DOM节点
  2. (fiber.effectTag & Placement) !== 0,即Fiber节点存在Placement effectTag

我们知道,mount时,fiber.stateNode === null,且在reconcileChildren中调用的mountChildFibers不会为Fiber节点赋值effectTag。那么首屏渲染如何完成呢?

针对第一个问题,fiber.stateNode会在completeWork中创建,我们会在下一节介绍。

第二个问题的答案十分巧妙:假设mountChildFibers也会赋值effectTag,那么可以预见mount时整棵Fiber树所有节点都会有Placement effectTag。那么commit阶段在执行DOM操作时每个节点都会执行一次插入操作,这样大量的DOM操作是极低效的。

为了解决这个问题,在mount时只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。

验证DEMO

借用上一节的Demo,第一个进入beginWork方法的Fiber节点就是rootFiber,他的alternate指向current rootFiber(即他存在current)。
为什么rootFiber节点存在current(即rootFiber.alternate),我们在双缓存机制一节mount时的第二步已经讲过
由于存在current,rootFiber在reconcileChildren时会走reconcileChildFibers逻辑。
而之后通过beginWork创建的Fiber节点是不存在current的(即 fiber.alternate === null),会走mountChildFibers逻辑
image.png

归-competeWork

上面我们知道组件执行beginWork后会创建子Fiber节点,节点上可能存在effectTag。

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...省略
      return null;
    }
    case HostRoot: {
      // ...省略
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
  // ...省略

重点关注页面渲染所必须的HostComponent(即原生DOM组件对应的Fiber节点)

处理HostComponent

case HostComponent: {
  popHostContext(workInProgress);
  const rootContainerInstance = getRootHostContainer();
  const type = workInProgress.type;



  if (current !== null && workInProgress.stateNode != null) {
    // update的情况
    // ...省略
  } else {
    // mount的情况
    // ...省略
  }
  return null;
}

根据current === null ?判断是mount还是update,同时针对HostComponent,判断update时我们还需要考虑workInProgress.stateNode != null ?(即该Fiber节点是否存在对应的DOM节点)

当update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。
需要做的主要是处理props,比如:

  • onClick、onChange等回调函数的注册
  • 处理style prop
  • 处理DANGEROUSLY_SET_INNER_HTML prop
  • 处理children prop

我们去掉一些当前不需要关注的功能(比如ref)。可以看到最主要的逻辑是调用updateHostComponent方法。

workInProgress.updateQueue = (updatePayload: any);

在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。

mount阶段主要逻辑包括三个:

  • 为Fiber节点生成对应的DOM节点
  • 将子孙DOM节点插入刚生成的DOM节点中
  • 与update逻辑中的updateHostComponent类似的处理props的过程 ```javascript // mount的情况

// …省略服务端渲染相关逻辑

const currentHostContext = getHostContext(); // 为fiber创建对应DOM节点 const instance = createInstance( type, newProps, rootContainerInstance, currentHostContext, workInProgress, ); // 将子孙DOM节点插入刚生成的DOM节点中 appendAllChildren(instance, workInProgress, false, false); // DOM节点赋值给fiber.stateNode workInProgress.stateNode = instance;

// 与update逻辑中的updateHostComponent类似的处理props的过程 if ( finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, ) ) { markUpdate(workInProgress); }

上面我们讲到:mount时只会在rootFiber存在 Placement effectTag。那么commit阶段是如何通过一次插入DOM操作(对应一个Placement effectTag)将整棵DOM树插入页面的呢?

原因就在于completeWork中的appendAllChildren方法。

由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到rootFiber时,我们已经有一个构建好的离屏DOM树。

还有一个问题:作为DOM操作的依据,commit阶段需要找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。难道需要在commit阶段再遍历一次Fiber树寻找effectTag !== null的Fiber节点么?<br />这显然是很低效的。

为了解决这个问题,在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中。

effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。<br />类似appendAllChildren,在“归”阶段,所有有effectTag的Fiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。
```javascript
                     nextEffect         nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber

这样,在commit阶段只需要遍历effectList就能执行所有effect了。

image.png