React 源码阅读 - 协调

570 阅读9分钟

什么是协调

Reconciliation 协调,也可以翻译成调和,他主要负责找出哪些组件发生了变化,鼎鼎大名的 diff 算法就是在这个阶段使用的。协调阶段即可以生成 Fiber,也可以更新 Fiber,协调阶段执行完,虚拟 DOM 生成或更新完,并会在内存中生成好虚拟 DOM 对应的真实 DOM。

Reconciler 工作的阶段被称为 render 阶段。因为在该阶段会调用组件的 render 方法。

三种模式

看源码之前我们先要知道 React 启动的三种模式,有助于我们对源码脉络的理解。

在 React 16 之后,React 都有 3 种启动方式:

  • legacy 模式,ReactDOM.render(<App />, rootNode),这是我们用得最多的模式传统模式,内部采用同步模式,这个模式下调度基本上不起作用;
  • blocking 模式,ReactDOM.createBlockingRoot(rootNode).render(<App />),迁移到 concurrent 模式的一个过渡形态,实际用得不多;
  • concurrent 模式,ReactDOM.createRoot(rootNode).render(<App />),预计在 React 18 会正式发布,并发模式下回开启所有新功能。

源码分析

React.render 为例,协调阶段始于 performSyncWorkOnRoot 函数(前面还有一些调用,如何调用到这里的先暂时不管),从名字上就可以看出,这是从 根 Fiber 开始执行同步任务。

整体调用过程如下图所示:

ReactDOM.render.png

beginWork

performSyncWorkOnRoot 函数中有一个关键调用是 renderRootSync ,然后他又调用了 prepareFreshStack,作用是重置一个新的堆栈环境。然后又调用了 createWorkInProgress,创建一个 WorkInProgress,可以理解为就是创建了一个新的任务,WorkInProgress 是一个全局变量,这也是任务的开始。

// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// The fiber we're working on
let workInProgress: Fiber | null = null;

function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
  // 省略一些代码
  workInProgressRoot = root;
  workInProgress = createWorkInProgress(root.current, null);
  // 省略一些代码
}

执行完 prepareFreshStack,又调用了workLoopSync

// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

可以看出全局变量 workInProgress 只要不为空就会一直调用 performUnitOfWork函数,这是一个深度优先搜索的过程。

// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function performUnitOfWork(unitOfWork: Fiber): void {
  // The current, flushed, state of this fiber is the alternate. Ideally
  // nothing should rely on this, but relying on it here means that we don't
  // need an additional field on the work in progress.
  const current = unitOfWork.alternate;
  setCurrentDebugFiberInDEV(unitOfWork);

  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }

  resetCurrentDebugFiberInDEV();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }

  ReactCurrentOwner.current = null;
}

这个函数有一个比较重要的调用就是 beginWork,他会返回下一个 workInProgress(也是一个 Fiber 对象,是上一个的子),然后进入下一个循环, 如果没有下一个就返回空,并进入到下一个阶段。

这个可以看一下 beginWork 函数的入参:

  • currentunitOfWork.alternate 也就是上一次渲染的 Fiber
  • unitOfWork(workInProgress) 是当前的正在进行处理的 Fiber
  • subtreeRenderLanes(renderLanes) 是赛道优先级相关的内容,暂不讨论
// 路径:packages/react-reconciler/src/ReactFiberBeginWork.new.js
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  let updateLanes = workInProgress.lanes;

  // 省略一些代码
  // current 不为空说明是更新,update时:如果 current 存在可能存在优化路径,可以复用 current(即上一次更新的 Fiber 节点)
  if (current !== null) {
    // 省略一些代码

    // 获取新旧 props
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    // 若 props 更新或者上下文改变,则认为需要"接受更新"
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // 打个更新标
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) { // 优先级不够,不需要更新的情况
      didReceiveUpdate = false;
      switch (workInProgress.tag) {
        // 省略一些代码
      }
      // 复用current
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    } else {
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        // 需要更新的情况
        didReceiveUpdate = true;
      } else {
        // 不需要更新的其他情况,这里我们的首次渲染就将执行到这一行的逻辑
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
  }

  // 省略一些代码
  // 这坨 switch 是 beginWork 中的核心逻辑
  // 首次挂载 mount时:根据 tag 不同,创建不同的子 Fiber 节点
  switch (workInProgress.tag) {
    // 省略一些代码
    // 中间一堆 case 就是处理不同类型的节点的,比如:函数组件、Class 组件、纯文本、根节点......
    case HostRoot:
    // ...省略
    case HostComponent:
    // 最后是一段错误处理兜底
}

通过 current 是否为空可以来判断这是 mount(首次加载) 还是 update(更新)阶段。

HostComponent(处理一些原生的 DOM 节点) 为例,进入 case 之后会调用 updateHostComponent 函数,里面有有一个关键调用就是 reconcileChildren

mount 时,调用 mountChildFibers,创建 子 Fiber

update 时,调用 reconcileChildFibers,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber 节点

下面是 mountChildFibersreconcileChildFibers 的定义:

// 路径:packages/react-reconciler/src/ReactChildFiber.new.js
function ChildReconciler(shouldTrackSideEffects) {
  function deleteChild // ...
  function placeChild // ...
  function placeSingleChild // ...
  function createChild // ...
  // ...
  // 省略一些代码
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    // 省略一些代码
  }
  
  return reconcileChildFibers;
}

export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

ChildReconciler 函数内部比较复杂其逻辑总结一下主要做了以下几件事情:

  1. 入参 shouldTrackSideEffects,字面意思就是”是否应该追踪副作用“,所以 update 的时候就是要追踪副作用,mount 的时候就是不追踪副作用;
  2. ChildReconciler 中定义了大量如 placeXXX、deleteXXX、updateXXX、reconcileXXX 等这样的函数,这些函数覆盖了对 Fiber 节点的创建、增加、删除、修改等动作,将直接或间接地被 reconcileChildFibers 所调用;
  3. ChildReconciler 的返回值是一个名为 reconcileChildFibers 的函数,这个函数是一个逻辑分发器,它将根据入参的不同,执行不同的 Fiber 节点操作,最终返回不同的目标 Fiber 节点。

第一点中的副作用是什么呢?以 placeSingleChild 为例,以下是 placeSingleChild 的源码:

function placeSingleChild(newFiber: Fiber): Fiber {
  // This is simpler for the single child case. We only need to do a
  // placement for inserting new children.
  if (shouldTrackSideEffects && newFiber.alternate === null) {
    newFiber.flags |= Placement;
  }
  return newFiber;
}

可以看出,一旦判断 shouldTrackSideEffects 为 false,那么下面所有的逻辑都不执行了,直接返回。如果执行下去会给 Fiber 节点打上一个叫 flags 的标记,像这样:

// 17 版本之前 flags 是 effectTag
newFiber.flags |= Placement;

Placement 的定义:

// 路径:packages/react-reconciler/src/ReactFiberFlags.js
export const Placement = /*                    */ 0b00000000000000000000010;
export const Update = /*                       */ 0b00000000000000000000100;
export const PlacementAndUpdate = /*           */ Placement | Update;
export const Deletion = /*                     */ 0b00000000000000000001000;
// 省略一些代码

flags(effectTag) 记录的是副作用的类型,而所谓“副作用”,React 给出的定义是“数据获取、订阅或者修改 DOM”等动作。

使用副作用的目的是什么呢?

简而言之只用副作用的目的就是提升页面更新的效率,没有 effectTag 标记的节点就不会被修改。

mount时只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。

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

总结一下, beginWork 阶段就是从 根 Fiber 开始,一路深度优先遍历,生成或更新Fiber树

completeWork

从上面 performUnitOfWork 函数可以看出一旦 beginWork 返回 null 就会执行 completeUnitOfWork 函数:

// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function completeUnitOfWork(unitOfWork: Fiber): void {
  // Attempt to complete the current unit of work, then move to the next
  // sibling. If there are no more siblings, return to the parent fiber.
  let completedWork = unitOfWork;
  do {
    // The current, flushed, state of this fiber is the alternate. Ideally
    // nothing should rely on this, but relying on it here means that we don't
    // need an additional field on the work in progress.
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;

    // Check if the work completed or if something threw.
    if ((completedWork.flags & Incomplete) === NoFlags) {
      setCurrentDebugFiberInDEV(completedWork);
      let next;
      if (
        !enableProfilerTimer ||
        (completedWork.mode & ProfileMode) === NoMode
      ) {
        next = completeWork(current, completedWork, subtreeRenderLanes);
      } else {
        startProfilerTimer(completedWork);
        next = completeWork(current, completedWork, subtreeRenderLanes);
        // Update render duration assuming we didn't error.
        stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
      }
      resetCurrentDebugFiberInDEV();

      if (next !== null) {
        // Completing this fiber spawned new work. Work on that next.
        workInProgress = next;
        return;
      }
    } else {
      // 省略一些代码
      if (next !== null) {
        next.flags &= HostEffectMask;
        workInProgress = next;
        return;
      }
      // 省略一些代码
    }

    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      workInProgress = siblingFiber;
      return;
    }
    // Otherwise, return to the parent
    completedWork = returnFiber;
    // Update the next thing we're working on in case something throws.
    workInProgress = completedWork;
  } while (completedWork !== null);

  // We've reached the root.
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

从上面代码可以看出在 completeWork 阶段,当前 Fiber 处理完成之后,如果有兄弟节点的话,会去找兄弟节点 siblingFiber,否则,会返回其父节点也就是 returnFiber,直到 根Fiber

兄弟节点肯定是没有走过 beginWork 的,所以,会跳回到 beginWork 函数,对其兄弟节点再进行一次深度优先遍历。

completeUnitOfWork 函数中还有一些对 Fiber 的 flags、subtreeFlags、deletions 等属性做操作的逻辑,标志着当前节点需不需要更新或者删除等,这个要是为了在后面提交的时候可以直接使用当前阶段的成果。

值得关注的是这个里面有个关键调用就是 completeWork 函数:

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:
      bubbleProperties(workInProgress);
      return null;
    case ClassComponent: {
      // 省略一些代码
    case HostRoot: {
      // 省略一些代码
    }
    case HostComponent: {
      popHostContext(workInProgress);
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;
      if (current !== null && workInProgress.stateNode != null) {
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );

        if (current.ref !== workInProgress.ref) {
          markRef(workInProgress);
        }
      } else {
        if (!newProps) {
          invariant(
            workInProgress.stateNode !== null,
            'We must have new props for new mounts. This error is likely ' +
              'caused by a bug in React. Please file an issue.',
          );
          // This can happen when we abort work.
          bubbleProperties(workInProgress);
          return null;
        }

        const currentHostContext = getHostContext();

        const wasHydrated = popHydrationState(workInProgress);
        if (wasHydrated) {
          if (
            prepareToHydrateHostInstance(
              workInProgress,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            // If changes to the hydrated node need to be applied at the
            // commit-phase we mark this as such.
            markUpdate(workInProgress);
          }
        } else {
          const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );

          appendAllChildren(instance, workInProgress, false, false);

          workInProgress.stateNode = instance;

          if (
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            markUpdate(workInProgress);
          }
        }

        if (workInProgress.ref !== null) {
          // If there is a ref on a host node we need to schedule a callback
          markRef(workInProgress);
        }
      }
      bubbleProperties(workInProgress);
      return null;
    }
    case HostText: {
      // 省略一些代码
    }

  }
  // 省略一些代码
}

completeWork 函数主要是一堆 switch case 语句构成,会根据 workInProgress 节点的 tag 属性不同,进入到不同的 DOM 节点创建、处理逻辑,比如:HostComponent 就是指的原生 DOM 元素类型。

completeWork 内部有三个关键动作:

  1. 创建 DOM 节点(createInstance);
  2. 将 DOM 节点插入到 DOM 树中(appendAllChildren);
  3. 为 DOM 节点设置属性(FinalizeInitialChildren)。

另外,创建好的 DOM 节点会被赋值到对应的 Fiber 的 stateNode 属性。

综上所述,在 completeWork 阶段主要做的事情就是负责处理 Fiber 节点到 DOM 节点的映射。

实例解读

import React from 'react';
import ReactDOM from 'react-dom';

function App() {
  return (
    <div className="App">
      <div className="container">
        <h1>Hello, Eagle</h1>
        <p>text1</p>
        <p>text2</p>
      </div>
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

下图是以上组件协调阶段的执行过程,可以帮助我们更好地理解 beginWork 的深度优先搜索过程,并且可以看到 beginWorkcompleteWork 其实是交替执行的。

另外,父子 Fiber 之间使用 returnchild 属性连接,兄弟 Fiber 之间使用 sibling 属性进行连接。

协调过程.png

至此,协调阶段全部工作完成。在 performSyncWorkOnRoot 函数中 fiberRootNode 被传递给 commitRoot 方法,开启 commit阶段 工作流程,也就是 Renderer 的工作。