Appearance
第6章 Commit 阶段:从虚拟到真实
本章要点
- Commit 阶段的三个子阶段:BeforeMutation、Mutation、Layout
- 为什么 Commit 阶段必须是同步的——不可中断的 DOM 操作
- Placement、Update、Deletion 三种 effect 的执行路径
commitRoot的完整源码解读与执行流程useLayoutEffect与useEffect的调度时机差异- Ref 的绑定与解绑发生在哪个子阶段
- React 如何保证 DOM 操作的原子性与一致性
- Passive Effects(useEffect)的异步调度机制
如果说 Reconciliation 是 React 的"参谋部"——负责分析形势、制定作战计划,那么 Commit 阶段就是"前线部队"——负责将计划执行为真实的 DOM 操作。在上一章中,我们看到 Diff 算法如何为每个需要变更的 Fiber 节点打上 Placement、Update、Deletion 等 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 树的构建那样可以随时丢弃重来——每一次 appendChild、removeChild、insertBefore 都会立即修改用户可见的 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 之前。这意味着:
- 在 BeforeMutation 和 Mutation 阶段,
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;
}
}
}
}