协调是 React 中最核心的机制之一,它负责回答一个关键问题:当组件状态发生变化时,如何高效地更新界面? 协调的完整流程包含了从触发更新到最终 DOM 刷新的一系列步骤。理解这个流程,能让你真正看懂 React 的“内心活动”,在性能优化和问题排查时游刃有余。
一句话概括协调
协调就是 React 将最新的状态与当前的 UI 描述进行比较,计算出最少更新操作,并将其应用到真实 DOM 的过程。
协调的四大阶段
整个协调流程可以拆分为四个阶段:触发更新 → 调度阶段 → Render 阶段 → Commit 阶段。
触发更新 → 调度阶段 → Render 阶段(可中断)→ Commit 阶段(同步不可中断)
下面我们一步步拆解。
阶段一:触发更新
协调的起点是状态更新。常见的触发方式包括:
useState的setStateuseReducer的dispatch- 组件顶层调用
ReactDOM.createRoot(...).render() this.setState(类组件,已逐渐退出历史舞台)
当这些方法被调用时,React 会创建一个更新对象(Update),将其放入对应 Fiber 节点的更新队列中,并启动调度流程。
function Counter() {
const [count, setCount] = useState(0);
// 点击按钮会触发更新
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
在这个例子中,setCount(c => c + 1) 创建了一个更新对象,描述“将 count 增加 1”。这个更新会被附加到 Counter 组件对应的 Fiber 节点上。
阶段二:调度阶段(Scheduler)
更新被创建后,并不会立即执行。React 使用调度器(Scheduler)来决定何时执行这个更新。
调度器基于 Lane 模型(车道模型)为更新分配优先级:
- 同步优先级:用户输入、动画等需要立即响应的操作
- 默认优先级:普通的数据请求、界面更新
- 低优先级:数据预加载等非紧急任务
React 会选择一个合适的“车道”处理更新,高优先级的更新会打断低优先级的 Render 阶段,这是 React 18 并发渲染的基础。
在调度阶段,React 会利用浏览器的空闲时间(通过 requestIdleCallback 或 MessageChannel 模拟),把渲染任务切分成多个时间片(Time Slicing),避免长时间占用主线程造成掉帧。
阶段三:Render 阶段(可中断)
Render 阶段是协调的核心执行部分,它是一段纯计算、无副作用的过程。在这个阶段,React 会构建或更新 workInProgress 树,并执行 Diff 算法。
Render 阶段并非真正渲染到屏幕,而是“计算需要哪些变更”,因此可以随时被中断或重启。
3.1 构建 workInProgress 树
React 维护两棵 Fiber 树:
- current 树:对应当前屏幕上显示内容的 Fiber 树
- workInProgress 树:正在内存中构建的新树,完成后再切换为 current
每个 Fiber 节点都与一个组件实例或 DOM 元素对应。构建过程是深度优先的,会为每个节点执行 beginWork 和 completeWork。
3.2 节点协调(Diff)
当走到一个组件节点时,React 会比较新旧 Fiber:
- 类型不变,只更新属性
- 例如
<div className="a">→<div className="b">,React 会保留该 DOM 节点,仅更新className。
- 类型改变,直接替换
- 例如
<span>→<div>,旧节点全部销毁,新建div。
- 列表 Diff
- 使用双端对比算法,优先通过 key 识别可复用节点。
- 没有 key 或不稳定的 key 会导致大量不必要的销毁与重新创建。
在 Render 阶段,React 会对每个节点标记副作用类型(flags),如 Placement(新增)、Update(更新)、Deletion(删除)等,这些标记将在 Commit 阶段被消费。
3.3 可中断特性
因为 Render 阶段是纯计算,React 可以在浏览器的空闲时间片结束后中断工作,让主线程处理用户输入等高优先级任务。下次恢复时会从上次中断的 Fiber 节点继续执行。
阶段四:Commit 阶段(同步不可中断)
Render 阶段计算出的 workInProgress 树一旦完成,就会进入 Commit 阶段。Commit 阶段是同步且不可中断的,因为它需要操作真实 DOM,保证界面的连续性。
Commit 阶段又可细分为三个子阶段:
4.1 before mutation(变更前)
- 执行
useEffect的清理函数(上一次 effect 的 destroy)。 - 保存 DOM 快照,如滚动位置,避免后续操作丢失。
4.2 mutation(变更)
- 根据 Render 阶段标记的副作用 flags,执行真实的 DOM 操作:
- 删除节点
- 插入节点
- 更新属性
- 在这个阶段,界面会发生实际变化,React 会确保变更顺序正确。
4.3 layout(变更后)
- 调用
useLayoutEffect的回调(它在 DOM 变更后、浏览器绘制前同步执行,适合读取布局信息)。 - 将 workInProgress 树切换为 current 树,完成渲染循环。
- 调度
useEffect的回调(这些会在浏览器绘制后异步执行,不阻塞屏幕更新)。
完整流程的示意图(文字版)
[用户操作] → setState()
↓
[创建 Update] → 附加到 Fiber 节点
↓
[调度更新] → Scheduler 分配 Lane 优先级
↓
[Render 阶段] → 构建 workInProgress 树
- beginWork:比对节点类型
- reconcileChildren:Diff 列表
- completeWork:创建/更新 DOM 节点
- 标记副作用 flags
↓
[Commit 阶段] 同步执行
- before mutation:清理上一轮 effects
- mutation:执行 DOM 操作
- layout:执行 useLayoutEffect,切换 current 树
↓
[屏幕刷新] → 浏览器绘制新 UI
↓
[执行 useEffect 回调](异步不阻塞)
一个实例看懂全过程
考虑以下组件代码:
function App() {
const [show, setShow] = useState(false);
return (
<div>
<button onClick={() => setShow(!show)}>切换</button>
{show && <p>Hello React</p>}
</div>
);
}
当用户点击按钮,setShow(!show) 被调用:
- 触发更新:创建更新
{ action: false → true },附加到 App 对应的 Fiber。 - 调度:因为是事件回调,获得同步优先级,立即执行。
- Render 阶段:
- React 创建一个新的 App Fiber(workInProgress),重新执行 App 函数,得到新的 JSX。
- 比较新旧虚拟 DOM 树:
<div>类型不变,只更新属性(无变化);新增{show && <p>}导致多出一个子节点。 - Diff 算法找到变化:p 标签需要插入,标记 Placement 副作用。
- Commit 阶段:
- mutation 子阶段:在
<div>下插入<p>Hello React</p>。 - layout 子阶段:切换 current 树;useLayoutEffect 执行(这里没有)。
- 浏览器绘制,用户看到 “Hello React”。
- 稍后
useEffect(如果有)异步执行。
你需要记住的核心要点
- 协调是状态更新到 UI 绘制的完整管线,不是单一的 Diff 算法。
- Render 阶段可中断,Commit 阶段同步且不可中断。
- Fiber 架构使得协调可以异步、可中断,这是并发渲染的基石。
- 理解协调流程有助于你写出更合理的
useEffect、useLayoutEffect依赖,并懂得何时使用React.memo或useMemo优化。
协调的细节确实深奥,但对于日常开发而言,掌握这四个阶段和关键概念,你就能在性能调优和面试中胸有成竹。