Appearance
5.3 Commit 阶段:副作用的执行
经过 Render 阶段的精心计算,React 已经构建了一棵 work-in-progress Fiber 树,并在这棵树上标记了所有需要执行的变更,我们称之为“副作用”(Side Effects)。现在,是时候将这些虚拟的变更应用到真实世界了。这个过程就发生在 Commit 阶段。
Commit 阶段是 React 更新流程的最后一步,它的入口函数是 commitRoot。与可中断的 Render 阶段不同,Commit 阶段是完全同步、不可中断的。这是为了确保 DOM 的一致性,防止用户看到渲染不完整的、破碎的 UI。
commitRoot 的工作可以被清晰地划分为三个子阶段,每个阶段都有明确的职责。
javascript
// react/packages/react-reconciler/src/ReactFiberWorkLoop.js
function commitRoot(
root: FiberRoot,
finishedWork: Fiber,
// ...其他参数
) {
const subtreeHasEffects = (finishedWork.subtreeFlags & MutationMask) !== NoFlags;
const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;
if (rootHasEffect || subtreeHasEffects) {
// 1. Before Mutation 阶段
// 遍历 Fiber 树,调用 getSnapshotBeforeUpdate
commitBeforeMutationEffects(root, finishedWork);
// 2. Mutation 阶段
// 执行 DOM 的插入、更新、删除操作
commitMutationEffects(root, finishedWork, lanes);
// 将 current 指针指向新的 WIP 树,完成切换
root.current = finishedWork;
// 3. Layout 阶段
// 调用 componentDidMount, componentDidUpdate 和 useLayoutEffect
commitLayoutEffects(root, finishedWork, lanes);
} else {
// 如果没有副作用,直接完成切换
root.current = finishedWork;
}
// ...后续处理,如调度 Passive Effects
}1. Before Mutation 阶段
目标:在 DOM 发生实际变更之前,让组件有机会从 DOM 中读取一些信息。
这个阶段由 commitBeforeMutationEffects 函数负责。它会遍历 finishedWork 树,专门寻找带有 Snapshot 副作用标记的 ClassComponent。对于找到的每个组件,它会调用其 getSnapshotBeforeUpdate 生命周期方法。
javascript
// react/packages/react-reconciler/src/ReactFiberCommitWork.js
function commitBeforeMutationEffects(root: FiberRoot, firstChild: Fiber) {
let fiber = firstChild;
while (fiber !== null) {
if (fiber.flags & Snapshot) {
const current = fiber.alternate;
commitBeforeMutationEffectOnFiber(current, fiber);
}
// ...遍历子节点和兄弟节点
}
}
function commitBeforeMutationEffectOnFiber(current, finishedWork) {
switch (finishedWork.tag) {
case ClassComponent:
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
const prevProps = finishedWork.memoizedProps;
const prevState = finishedWork.memoizedState;
instance.getSnapshotBeforeUpdate(prevProps, prevState);
}
break;
// ...其他 case
}
}设计思考:为什么需要这个阶段?一个经典的案例是聊天应用的滚动条。在接收到新消息并渲染到列表之前,你可能需要记录下当前用户的滚动位置,以便在列表更新后恢复,避免新消息的插入导致页面“跳动”。getSnapshotBeforeUpdate 返回的值会作为 componentDidUpdate 的第三个参数,完美地解决了这个问题。
2. Mutation 阶段
目标:执行所有 DOM 的增、删、改操作。
这是 Commit 阶段最核心的部分,由 commitMutationEffects 函数执行。它会遍历副作用列表(一个在 Render 阶段构建的、只包含有副作用的 Fiber 的链表),并根据副作用的类型(Placement, Update, Deletion)执行相应的 DOM 操作。
Placement:插入新的 DOM 节点。React 会调用parent.insertBefore()或parent.appendChild()。Update:更新 DOM 节点的属性、样式或文本内容。Deletion:从 DOM 中移除一个节点。React 会调用parent.removeChild()。
javascript
// react/packages/react-reconciler/src/ReactFiberCommitWork.js
function commitMutationEffects(root: FiberRoot, finishedWork: Fiber, lanes: Lanes) {
// 遍历副作用列表并执行
commitMutationEffectsOnFiber(finishedWork, root, lanes);
}
function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
const flags = finishedWork.flags;
if (flags & Deletion) {
// 执行删除操作
commitDeletion(root, finishedWork, ...);
}
if (flags & Placement) {
// 执行插入操作
commitPlacement(finishedWork);
}
if (flags & Update) {
// 执行更新操作
commitWork(current, finishedWork);
}
// ...
}这个阶段是唯一会实际修改宿主环境(如 DOM)的部分。为了效率,React 会在 Render 阶段收集所有变更,然后在 Mutation 阶段一次性批量执行。
3. Layout 阶段
目标:在 DOM 更新完成后,执行需要访问 DOM 布局信息的代码,并调用相应的生命周期方法。
当 Mutation 阶段完成,新的 UI 已经真实地呈现在屏幕上。此时,commitLayoutEffects 函数开始工作。它会再次遍历 Fiber 树,执行以下任务:
- 调用
componentDidMount(对于新挂载的类组件)。 - 调用
componentDidUpdate(对于更新的类组件)。 - 同步执行
useLayoutEffect的回调函数。
javascript
// react/packages/react-reconciler/src/ReactFiberCommitWork.js
function commitLayoutEffects(root: FiberRoot, finishedWork: Fiber, lanes: Lanes) {
// 遍历 Fiber 树并执行 Layout Effects
commitLayoutEffects_begin(finishedWork, root, lanes);
}
function commitLayoutEffectOnFiber(finishedWork: Fiber, root: FiberRoot, lanes: Lanes) {
const flags = finishedWork.flags;
if (flags & (Update | Callback)) {
switch (finishedWork.tag) {
case ClassComponent:
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
instance.componentDidUpdate(...);
}
break;
case FunctionComponent:
// 执行 useLayoutEffect 的回调
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
break;
}
}
}设计思考:useLayoutEffect 与 useEffect 的关键区别就在于执行时机。useLayoutEffect 在浏览器绘制之前同步执行,因此它适合执行那些需要读取或修改 DOM 布局并希望用户看不到中间状态的操作(例如,测量元素尺寸并设置样式)。但因为它会阻塞浏览器绘制,所以应谨慎使用,优先选择 useEffect。
useEffect 的异步调度
你可能已经注意到,useEffect 并没有在上述三个阶段中被调用。这是因为 useEffect 被设计为异步执行,以避免阻塞浏览器渲染。在 commitRoot 的末尾,如果检测到有 Passive(useEffect 的内部代号)副作用,React 会使用调度器(Scheduler)调度一个低优先级的任务,在未来的某个时间点异步地执行 useEffect 的回调和清理函数。
这个设计确保了即使 useEffect 中有耗时操作,也不会影响到本次渲染的绘制,从而提升了用户感知的性能。
至此,一次完整的 React 更新流程——从 setState 到屏幕更新——就全部完成了。