Appearance
第 3.4 节:工作循环:Render 与 Commit 阶段的协同
在 scheduleUpdateOnFiber 将更新任务交给调度器后,React 的工作循环(Work Loop)正式启动。这个循环是 React 实现高性能和响应性的核心,它被划分为两个主要阶段:Render 阶段和 Commit 阶段。这两个阶段协同工作,将组件的状态变更最终呈现为用户界面的更新。
1. 工作循环的启动
当调度器决定执行一个任务时,它会调用 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot。这两个函数是工作循环的入口,它们内部会根据任务的性质(同步或并发)调用相应的 renderRoot 函数。
javascript
// packages/react-reconciler/src/ReactFiberWorkLoop.js
// 这是一个简化的逻辑,实际的函数是 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot
function performWorkOnRoot(root, lanes) {
// ...
const shouldTimeSlice = !includesSyncLane(lanes);
// 根据是否需要时间分片,决定进入同步渲染还是并发渲染
const exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
// ...
}renderRootSync:用于处理同步任务,例如由用户交互(如点击)触发的更新。它会一次性、不间断地完成整个 Render 阶段。renderRootConcurrent:用于处理可中断的并发任务,例如useTransition中的更新。它可以在渲染过程中暂停,将控制权交还给主线程,从而避免阻塞用户界面。
2. Render 阶段:构建“蓝图”
Render 阶段的主要目标是创建一个新的 Fiber 树,我们称之为“work-in-progress” (WIP) 树。这棵树是即将呈现的 UI 状态的“蓝图”。
此阶段的核心是 workLoopSync 或 workLoopConcurrent 函数,它们会遍历 Fiber 树,并为每个 Fiber 节点执行 performUnitOfWork。
javascript
// packages/react-reconciler/src/ReactFiberWorkLoop.js
function workLoopSync() {
// 只要有工作要做,就持续执行
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() {
// 在执行工作的同时,检查是否需要让出主线程
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}performUnitOfWork 内部会调用 beginWork,beginWork 的职责是:
- Diffing:比较当前 Fiber 节点和新的 React Element,找出差异。
- 创建子节点:根据
beginWork的结果,为子组件创建新的 Fiber 节点。 - 标记副作用:如果一个节点需要进行 DOM 操作(如插入、更新、删除),它会被打上一个特殊的标记(Flag),例如
Placement、Update或Deletion。
设计思考:为什么 Render 阶段可以被中断?
这是 React 并发模式的核心。通过将渲染工作拆分成许多小的“工作单元”(每个 Fiber 节点一次),React 可以在执行完一个单元后检查是否有更高优先级的任务(如用户输入)。如果有,它可以暂停当前的渲染,去处理更高优先级的任务,处理完后再回来继续之前的渲染。这使得应用在高负载下依然能保持流畅的交互响应。
Render 阶段完成后,会生成一个 finishedWork 对象,它就是构建完成的 WIP 树的根节点。这棵树上已经标记了所有需要执行的副作用。
3. Commit 阶段:执行变更
当 Render 阶段完成并且 finishedWork 树准备好后,工作循环进入 Commit 阶段。这个阶段由 commitRoot 函数负责,它的任务是将 Render 阶段计算出的变更应用到 DOM 上。
与 Render 阶段不同,Commit 阶段是完全同步的,不可中断。 这是为了保证 DOM 的一致性,避免用户看到渲染不完整的 UI。
commitRoot 函数的工作分为三个主要的子阶段:
javascript
// packages/react-reconciler/src/ReactFiberWorkLoop.js
function commitRoot(root, finishedWork) {
// ...
// 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, lanes);
// ...
}Before Mutation 阶段:
- 在这个阶段,React 会遍历
finishedWork树,为所有带有Snapshot副作用标记的节点调用getSnapshotBeforeUpdate生命周期方法。 - 这使得组件可以在 DOM 发生变更前,从 DOM 中读取一些信息(例如,滚动位置)。
- 在这个阶段,React 会遍历
Mutation 阶段:
- React 再次遍历
finishedWork树,执行所有真正的 DOM 操作。 - 它会根据 Render 阶段设置的副作用标记(
Placement,Update,Deletion)来执行appendChild、removeChild或修改节点属性等操作。
- React 再次遍历
Layout 阶段:
- 在 DOM 更新完成后,React 会最后一次遍历 Fiber 树,调用所有
componentDidMount、componentDidUpdate生命周期方法,以及useLayoutEffect的回调函数。 - 在这个阶段,组件已经渲染到了屏幕上,可以安全地执行需要访问 DOM 布局信息的代码。
- 在 DOM 更新完成后,React 会最后一次遍历 Fiber 树,调用所有
4. 协同工作:从蓝图到现实
Render 和 Commit 阶段的协同是 React 更新流程的核心:
- Render 阶段 像一个建筑师,它根据状态变更的“需求”,精心绘制出一份详细的“施工蓝图”(
finishedWork树),并在这份蓝图上标记出所有需要改动的地方。 - Commit 阶段 则像一个施工队,它接过这份蓝图,然后严格按照上面的标记,一次性、快速地完成所有实际的“施工”(DOM 操作),最终将新的 UI 呈现给用户。
这种分离的设计带来了巨大的好处:
- 并发与中断:将耗时的计算(Render)与实际的 DOM 修改(Commit)分开,使得 React 可以在不影响最终结果的情况下,安全地中断和恢复 Render 阶段。
- 一致性保证:通过让 Commit 阶段同步且不可中断,React 保证了用户永远不会看到一个只渲染了一半的、不一致的 UI。
在 Commit 阶段结束后,React 还会异步地调度 useEffect 的执行,这是为了避免阻塞渲染。至此,一次完整的更新循环就结束了。