人人都会AI编程

4.5 computed 计算属性:缓存机制与懒执行原理

更新时间:2026-07-10

协调是 React 中最核心的机制之一,它负责回答一个关键问题:当组件状态发生变化时,如何高效地更新界面? 协调的完整流程包含了从触发更新到最终 DOM 刷新的一系列步骤。理解这个流程,能让你真正看懂 React 的“内心活动”,在性能优化和问题排查时游刃有余。

一句话概括协调

协调就是 React 将最新的状态当前的 UI 描述进行比较,计算出最少更新操作,并将其应用到真实 DOM 的过程。

协调的四大阶段

整个协调流程可以拆分为四个阶段:触发更新 → 调度阶段 → Render 阶段 → Commit 阶段

触发更新 → 调度阶段 → Render 阶段(可中断)→ Commit 阶段(同步不可中断)

下面我们一步步拆解。


阶段一:触发更新

协调的起点是状态更新。常见的触发方式包括:

  • useStatesetState
  • useReducerdispatch
  • 组件顶层调用 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 会利用浏览器的空闲时间(通过 requestIdleCallbackMessageChannel 模拟),把渲染任务切分成多个时间片(Time Slicing),避免长时间占用主线程造成掉帧。


阶段三:Render 阶段(可中断)

Render 阶段是协调的核心执行部分,它是一段纯计算、无副作用的过程。在这个阶段,React 会构建或更新 workInProgress 树,并执行 Diff 算法。

Render 阶段并非真正渲染到屏幕,而是“计算需要哪些变更”,因此可以随时被中断或重启。

3.1 构建 workInProgress 树

React 维护两棵 Fiber 树:

  • current 树:对应当前屏幕上显示内容的 Fiber 树
  • workInProgress 树:正在内存中构建的新树,完成后再切换为 current

每个 Fiber 节点都与一个组件实例或 DOM 元素对应。构建过程是深度优先的,会为每个节点执行 beginWorkcompleteWork

3.2 节点协调(Diff)

当走到一个组件节点时,React 会比较新旧 Fiber:

  1. 类型不变,只更新属性
  • 例如 <div className="a"><div className="b">,React 会保留该 DOM 节点,仅更新 className
  1. 类型改变,直接替换
  • 例如 <span><div>,旧节点全部销毁,新建 div
  1. 列表 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) 被调用:

  1. 触发更新:创建更新 { action: false → true },附加到 App 对应的 Fiber。
  2. 调度:因为是事件回调,获得同步优先级,立即执行。
  3. Render 阶段
  • React 创建一个新的 App Fiber(workInProgress),重新执行 App 函数,得到新的 JSX。
  • 比较新旧虚拟 DOM 树:<div> 类型不变,只更新属性(无变化);新增 {show && <p>} 导致多出一个子节点。
  • Diff 算法找到变化:p 标签需要插入,标记 Placement 副作用。
  1. Commit 阶段
  • mutation 子阶段:在 <div> 下插入 <p>Hello React</p>
  • layout 子阶段:切换 current 树;useLayoutEffect 执行(这里没有)。
  • 浏览器绘制,用户看到 “Hello React”。
  • 稍后 useEffect(如果有)异步执行。

你需要记住的核心要点

  • 协调是状态更新到 UI 绘制的完整管线,不是单一的 Diff 算法。
  • Render 阶段可中断,Commit 阶段同步且不可中断。
  • Fiber 架构使得协调可以异步、可中断,这是并发渲染的基石。
  • 理解协调流程有助于你写出更合理的 useEffectuseLayoutEffect 依赖,并懂得何时使用 React.memouseMemo 优化。

协调的细节确实深奥,但对于日常开发而言,掌握这四个阶段和关键概念,你就能在性能调优和面试中胸有成竹。