Skip to content

React Commit Phase 完整指南

Commit Phase 是 React 更新流程的最后阶段,它负责将 Render Phase 计算出来的变更(记录在 finishedWork Fiber 树上)实际应用到 DOM 上。

Commit Phase 的入口函数是 。它接收 root (FiberRoot 对象)、finishedWork (渲染完成的 Fiber 树)等参数。

commitRoot 函数
javascript
function commitRoot(
  root: FiberRoot,
  finishedWork: null | Fiber,
  lanes: Lanes,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
  didIncludeRenderPhaseUpdate: boolean,
  spawnedLane: Lane,
  updatedLanes: Lanes,
  suspendedRetryLanes: Lanes,
  exitStatus: RootExitStatus,
  suspendedCommitReason: SuspendedCommitReason, // Profiling-only
  completedRenderStartTime: number, // Profiling-only
  completedRenderEndTime: number, // Profiling-only
): void {
  // ... 初始化和准备工作 ...

  // 标记 Root 完成状态,重置 workInProgressRoot 等
  markRootFinished(
    root,
    lanes,
    remainingLanes,
    spawnedLane,
    updatedLanes,
    suspendedRetryLanes,
  );

  // ... 其他准备工作,如处理 Profiler 日志,重置 commit 阶段更新标志 ...

  // 将 finishedWork 赋值给 pendingFinishedWork,用于后续处理
  pendingFinishedWork = finishedWork;
  pendingEffectsRoot = root;
  // ... 保存其他相关状态到 pendingXXX 变量 ...

  // 如果启用了 Passive Effects,并且 finishedWork 上有 PassiveMask 副作用标记,
  // 则调度一个回调来异步执行 Passive Effects。
  if ((finishedWork.subtreeFlags & passiveSubtreeMask) !== NoFlags ||
      (finishedWork.flags & passiveSubtreeMask) !== NoFlags) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      scheduleCallback(NormalPriority, () => {
        flushPassiveEffects();
        return null;
      });
    }
  }

  // 正式开始 Commit Phase 的三个子阶段
  const subtreeHasEffects = (finishedWork.subtreeFlags & commitEffectMask) !== NoFlags;
  const rootHasEffect = (finishedWork.flags & commitEffectMask) !== NoFlags;

  if (subtreeHasEffects || rootHasEffect) {
    // ** 阶段1: Before Mutation Effects **
    // -----------------------------------
    // 在实际 DOM 变更之前执行,主要用于读取 DOM 状态,
    // 例如执行类组件的 getSnapshotBeforeUpdate。
    // 也处理与 View Transitions 相关的准备工作。
    commitBeforeMutationEffects(root, finishedWork, lanes);

    // ** 阶段2: Mutation Effects **
    // ---------------------------
    // 执行实际的 DOM 插入、更新、删除操作。
    // 这个阶段会遍历 Fiber 树,根据 flags 执行对应的 DOM API 调用。
    commitMutationEffects(root, finishedWork, lanes);

    // 将 current 指针指向 finishedWork,完成 Fiber 树的切换
    root.current = finishedWork;

    // ** 阶段3: Layout Effects **
    // -------------------------
    // 在 DOM 变更之后,浏览器绘制之前同步执行。
    // 主要用于执行 useLayoutEffect Hook 的回调、类组件的 componentDidMount/Update。
    // 也处理 ref 的附加和分离。
    commitLayoutEffects(root, finishedWork, lanes);
  } else {
    // 如果没有副作用,直接更新 current 指针
    root.current = finishedWork;
  }

  // ... 清理工作,例如重置全局变量,处理 Profiler 提交后钩子 ...

  // 确保所有同步工作都已完成
  flushSyncWorkOnAllRoots(); 

  // ... 其他收尾工作,如处理 pending passive effects 的调度 ...
}
// ... existing code ...

Commit Phase 是同步执行的,不能被打断。 这个阶段的主要任务是将 Render Phase 计算出来的变更真实地应用到 DOM 上,并执行相关的生命周期方法(或 Hooks)。

在其内部大体上可以分为四个阶段,React 按顺序执行以下操作:

  • Before Mutation Effects: 这是在 DOM 突变之前执行的副作用。
  • DOM Mutations: 这是 React 实际将变更应用到 DOM 的阶段。
  • Layout Effects: 这是在 DOM 突变完成后,同步执行的副作用。
  • Passive Effects: 这是 React 用于异步执行副作用的阶段。

前置突变副作用(Before Mutation Effects)

React 允许组件在 DOM 即将被修改之前,从 DOM 中捕获一些信息(例如滚动位置)。这样,在 DOM 更新后,这些信息可以被用来恢复状态或进行其他必要的调整。

所以在实际操作 DOM 之前,执行一些需要读取 DOM 布局或状态的副作用。最典型的例子是 getSnapshotBeforeUpdate 这个生命周期方法(在类组件中)和 useLayoutEffect Hook 中需要读取 DOM 的部分。

在这个阶段中,React 会遍历effect list(在 Render Phase 构建的,标记了需要进行副作用的 fiber 节点列表)。

DOM 突变(DOM Mutations)

此阶段React 再次遍历 effect list,执行所有实际的 DOM 操作,如添加、删除、更新节点和属性。对于不同的操作,React 会执行以下步骤:

  • 插入: 对于标记为 Placement 的 fiber,React 会创建新的 DOM 节点并将其插入到父 DOM 节点中的正确位置。
  • 更新: 对于标记为 Update 的 fiber,React 会检查 props 的差异,并更新 DOM 节点的属性、样式、事件监听器等。
  • 删除: 对于标记为 Deletion 的 fiber,React 会将其对应的 DOM 节点从父 DOM 节点中移除。在移除前,会执行其子树中所有组件的 componentWillUnmount 生命周期方法和 useEffect / useLayoutEffect 的清理函数。
  • Ref 处理: 在这个阶段,ref 回调函数(对于 DOM 节点和类组件)或 useRef 创建的 ref 对象的 .current 属性会被更新,指向相应的 DOM 节点或组件实例。对于 ref 的卸载(当 ref 指向的节点被移除或 ref 函数改变时),清理 ref 的操作也会在 DOM 节点被移除前执行。

布局副作用(Layout Effects)

这些副作用通常用于需要同步读取或修改 DOM 布局的操作。例如,测量元素尺寸、设置焦点、或者同步触发动画。所以在 DOM 突变完成之后,同步执行所有 useLayoutEffect Hook 的回调函数,以及类组件的 componentDidMountcomponentDidUpdate 生命周期方法。

因为它们是同步执行的,所以可以确保在浏览器下一次绘制之前完成,避免视觉不一致。

副作用(Passive Effects / Effects)

在 Layout Effects 执行完毕后,如果存在 Passive Effects (主要由 useEffect Hook 注册),React 会异步调度一个任务来执行它们。这个任务会在浏览器绘制之后、主线程空闲时执行,以避免阻塞渲染。

React 会调度一个任务来异步执行 useEffect, 执行上一次 Render 中注册的并且其依赖项已改变的 useEffect 的清理函数。如果组件正在卸载,则执行所有 useEffect 的清理函数。然后,执行本次 Render 中注册的并且其依赖项已改变(或者没有依赖项)的 useEffect 的回调函数。

关于组件卸载时的清理:

当一个组件被卸载时(在 DOM Mutations 阶段被标记为 Deletion):

  • 首先会执行其类组件子孙的 componentWillUnmount
  • 然后会执行其自身及子孙的 useLayoutEffect 的清理函数(同步)。
  • 最后,其自身及子孙的 useEffect 的清理函数会被调度执行(异步,在 Passive Effects 阶段)。

Commit Phase 流程图


微信公众号二维码