Skip to content

第6章 Commit 阶段:从虚拟到真实

本章要点

  • Commit 阶段的三个子阶段:BeforeMutation、Mutation、Layout
  • 为什么 Commit 阶段必须是同步的——不可中断的 DOM 操作
  • Placement、Update、Deletion 三种 effect 的执行路径
  • commitRoot 的完整源码解读与执行流程
  • useLayoutEffectuseEffect 的调度时机差异
  • Ref 的绑定与解绑发生在哪个子阶段
  • React 如何保证 DOM 操作的原子性与一致性
  • Passive Effects(useEffect)的异步调度机制

如果说 Reconciliation 是 React 的"参谋部"——负责分析形势、制定作战计划,那么 Commit 阶段就是"前线部队"——负责将计划执行为真实的 DOM 操作。在上一章中,我们看到 Diff 算法如何为每个需要变更的 Fiber 节点打上 PlacementUpdateDeletion 等 flags。现在,是时候看看这些 flags 如何被翻译成浏览器真正理解的 DOM API 调用了。

Commit 阶段的设计有一个核心约束:它必须是同步的、不可中断的。这与 Render 阶段形成了鲜明对比。Render 阶段可以被更高优先级的任务打断、可以重新开始、甚至可以丢弃中间结果。但 Commit 阶段一旦开始,就必须一气呵成——因为用户不能看到"DOM 改了一半"的中间状态。

6.1 为什么 Commit 必须同步

想象一个简单的场景:你要把列表从 [A, B, C] 更新为 [A, C, B]。这需要两步 DOM 操作:移动 C 到 B 前面,或者移动 B 到 C 后面。如果在执行完第一步之后被中断了会怎样?

tsx
// 列表更新:[A, B, C] → [A, C, B]
// 需要的 DOM 操作:
// 1. 将 C 移动到 B 之前
// 2. (或者等价地) 将 B 移动到最后

// 如果在步骤1之后被中断:
// 用户看到的是 [A, C, B, C] 还是 [A, C] ?
// 无论哪种,都是错误的中间状态

这就是 Commit 阶段必须同步执行的根本原因。DOM 操作不像 Fiber 树的构建那样可以随时丢弃重来——每一次 appendChildremoveChildinsertBefore 都会立即修改用户可见的 DOM 树。中间状态的暴露不仅会导致视觉闪烁,更可能引起布局抖动(Layout Thrashing),在严重情况下甚至会导致事件处理器绑定到错误的 DOM 节点上。

图 6-1:Render 阶段与 Commit 阶段的执行模式对比

6.2 commitRoot 的整体结构

当 Render 阶段完成后,React 会调用 commitRoot 启动 Commit 阶段。这是整个 Commit 的入口,让我们看看它的核心结构:

typescript
// 简化版 commitRoot
function commitRoot(root: FiberRoot) {
  const finishedWork = root.finishedWork;
  if (finishedWork === null) return;

  // 重置状态
  root.finishedWork = null;
  root.finishedLanes = NoLanes;

  // 检查是否存在 passive effects(useEffect)
  // 如果有,调度一个异步任务去执行它们
  if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
  ) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        return null;
      });
    }
  }

  // 判断是否有需要处理的副作用
  const subtreeHasEffects =
    (finishedWork.subtreeFlags & MutationMask | LayoutMask | PassiveMask) !== NoFlags;
  const rootHasEffect =
    (finishedWork.flags & MutationMask | LayoutMask | PassiveMask) !== NoFlags;

  if (subtreeHasEffects || rootHasEffect) {
    // ========== 第一阶段:BeforeMutation ==========
    commitBeforeMutationEffects(root, finishedWork);

    // ========== 第二阶段:Mutation ==========
    commitMutationEffects(root, finishedWork);

    // 关键:在 Mutation 和 Layout 之间切换 Fiber 树
    root.current = finishedWork;

    // ========== 第三阶段:Layout ==========
    commitLayoutEffects(finishedWork, root);
  } else {
    // 没有副作用,直接切换 Fiber 树
    root.current = finishedWork;
  }

  // 调度可能的后续更新
  ensureRootIsScheduled(root);
}

注意代码中那行看似不起眼的 root.current = finishedWork。这是 React 双缓冲机制的关键——它将"当前显示的 Fiber 树"从 current 切换到了 finishedWork(也就是 workInProgress 树)。这个切换的位置至关重要:它发生在 Mutation 之后、Layout 之前。这意味着:

  • BeforeMutationMutation 阶段,root.current 仍然指向旧树(可以读取旧的 state 和 props)
  • Layout 阶段,root.current 已经指向新树(读取到的是新的 state 和 props)

这个设计保证了生命周期方法能在正确的时机读取到正确的值。

6.3 BeforeMutation 阶段:最后的准备

BeforeMutation 是 Commit 阶段的第一幕。在这个阶段,DOM 尚未被修改,React 需要做一些"变更前的准备工作"。

typescript
function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber
) {
  nextEffect = firstChild;
  // 深度优先遍历 Fiber 树
  commitBeforeMutationEffects_begin();
}

function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    const fiber = nextEffect;

    try {
      commitBeforeMutationEffectsOnFiber(fiber);
    } catch (error) {
      captureCommitPhaseError(fiber, fiber.return, error);
    }

    const sibling = fiber.sibling;
    if (sibling !== null) {
      nextEffect = sibling;
      return; // 回到 _begin 处理兄弟节点
    }
    nextEffect = fiber.return;
  }
}

function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;

  // 1. 处理 getSnapshotBeforeUpdate
  if ((flags & Snapshot) !== NoFlags) {
    switch (finishedWork.tag) {
      case ClassComponent: {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
          // 调用 getSnapshotBeforeUpdate
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState
          );
          // 保存快照值,后续在 componentDidUpdate 中作为第三个参数传入
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
        break;
      }
      case HostRoot: {
        // 如果 root 的容器需要清空内容
        if (supportsMutation) {
          const container = finishedWork.stateNode.containerInfo;
          clearContainer(container);
        }
        break;
      }
    }
  }
}

基于 VitePress 构建