Skip to content

第 3.4 节:工作循环:Render 与 Commit 阶段的协同

scheduleUpdateOnFiber 将更新任务交给调度器后,React 的工作循环(Work Loop)正式启动。这个循环是 React 实现高性能和响应性的核心,它被划分为两个主要阶段:Render 阶段Commit 阶段。这两个阶段协同工作,将组件的状态变更最终呈现为用户界面的更新。

1. 工作循环的启动

当调度器决定执行一个任务时,它会调用 performSyncWorkOnRootperformConcurrentWorkOnRoot。这两个函数是工作循环的入口,它们内部会根据任务的性质(同步或并发)调用相应的 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 状态的“蓝图”。

此阶段的核心是 workLoopSyncworkLoopConcurrent 函数,它们会遍历 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 内部会调用 beginWorkbeginWork 的职责是:

  1. Diffing:比较当前 Fiber 节点和新的 React Element,找出差异。
  2. 创建子节点:根据 beginWork 的结果,为子组件创建新的 Fiber 节点。
  3. 标记副作用:如果一个节点需要进行 DOM 操作(如插入、更新、删除),它会被打上一个特殊的标记(Flag),例如 PlacementUpdateDeletion

设计思考:为什么 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);

  // ...
}
  1. Before Mutation 阶段

    • 在这个阶段,React 会遍历 finishedWork 树,为所有带有 Snapshot 副作用标记的节点调用 getSnapshotBeforeUpdate 生命周期方法。
    • 这使得组件可以在 DOM 发生变更前,从 DOM 中读取一些信息(例如,滚动位置)。
  2. Mutation 阶段

    • React 再次遍历 finishedWork 树,执行所有真正的 DOM 操作。
    • 它会根据 Render 阶段设置的副作用标记(Placement, Update, Deletion)来执行 appendChildremoveChild 或修改节点属性等操作。
  3. Layout 阶段

    • 在 DOM 更新完成后,React 会最后一次遍历 Fiber 树,调用所有 componentDidMountcomponentDidUpdate 生命周期方法,以及 useLayoutEffect 的回调函数。
    • 在这个阶段,组件已经渲染到了屏幕上,可以安全地执行需要访问 DOM 布局信息的代码。

4. 协同工作:从蓝图到现实

Render 和 Commit 阶段的协同是 React 更新流程的核心:

  • Render 阶段 像一个建筑师,它根据状态变更的“需求”,精心绘制出一份详细的“施工蓝图”(finishedWork 树),并在这份蓝图上标记出所有需要改动的地方。
  • Commit 阶段 则像一个施工队,它接过这份蓝图,然后严格按照上面的标记,一次性、快速地完成所有实际的“施工”(DOM 操作),最终将新的 UI 呈现给用户。

这种分离的设计带来了巨大的好处:

  • 并发与中断:将耗时的计算(Render)与实际的 DOM 修改(Commit)分开,使得 React 可以在不影响最终结果的情况下,安全地中断和恢复 Render 阶段。
  • 一致性保证:通过让 Commit 阶段同步且不可中断,React 保证了用户永远不会看到一个只渲染了一半的、不一致的 UI。

在 Commit 阶段结束后,React 还会异步地调度 useEffect 的执行,这是为了避免阻塞渲染。至此,一次完整的更新循环就结束了。

Last updated: