人人都会AI编程

6.4 组件更新渲染全流程:数据变更 → 异步更新队列 → Diff 对比 → 局部 DOM 更新

更新时间:2026-07-10

前面我们分别拆解了 setState / useState 的更新机制、批量更新原理以及更新队列的合并规则。这一节我们将这些知识点串起来,用一条完整的链路展示:当你调用 setStateuseState 的更新函数后,React 内部究竟发生了什么,直到最终屏幕上显示出新的 UI。

理解这个流程,你就能真正吃透 React 的渲染机制,定位性能瓶颈时也不再只能靠猜。

6.4.1 总览:整个流程的四大阶段

整个状态更新到视图刷新的过程可以划分为四个关键阶段:

  1. 触发更新(Trigger):调用 setState / dispatch,创建 Update 对象并加入更新队列。
  2. 调度更新(Schedule):Scheduler 根据优先级协调任务,决定何时开始渲染。
  3. 渲染阶段(Render):React 重新执行组件函数,计算新的虚拟 DOM,并与旧 Fiber 树进行 Diff,标记变更。
  4. 提交阶段(Commit):将 Render 阶段计算出的变更同步到真实 DOM,并执行副作用(Effects)。

对于用户来说,最直接的感知是“状态变了,界面不久后跟着刷新”。下面我们逐步展开每个阶段的具体细节。


6.4.2 阶段一:触发——从 setState 到 Update 入队

无论是类组件中的 this.setState,还是函数组件中 useState 返回的 setCount,它们内部最终都会调用同一个核心方法 enqueueSetState(类组件)或 dispatchAction(函数组件)。

以函数组件为例,当你执行 setCount(count + 1) 时:

  1. 创建一个 Update 对象,其中包含新的状态值(或状态更新函数)。
  2. 这个 Update 被加入到当前 Fiber 节点的 updateQueue(一个环形链表)中。
  3. React 将当前 Fiber 节点标记为“有待处理的更新”。
// 伪代码:Update 对象的大致结构
const update = {
  lane: SyncLane,          // 优先级(此处同步为例)
  payload: newState,       // 新值或 prevState => newState 函数
  next: null,               // 指向下一个 update
};

此时组件函数还没有重新执行,状态的实际值仍是旧的。

6.4.3 阶段二:调度——Scheduler 协调任务优先级

更新入队后,React 不会立即执行渲染,而是通过调度器(Scheduler)来安排合适的执行时机。

  • 调度器会根据更新的优先级(Lane 模型)决定何时开始工作。
  • 对于 SyncLane(同步更新,如 setState 在合成事件或生命周期内),React 会将工作安排在微任务中(React 18 并发模式下,如果使用了 createRoot,同步更新也可能被调度)。
  • 对于低优先级的 Concurrent 更新(例如 startTransition 包裹的更新),工作可能会被分片,利用浏览器空闲时间逐帧完成。

调度的核心目的是避免主线程长时间阻塞,保证浏览器有时间响应用户输入(如点击、滚动),从而保持页面流畅。

6.4.4 阶段三:Render——执行组件函数,Diff 产出 Effect List

当调度器决定执行工作时,React 进入 Render 阶段(又称 Reconciliation 阶段)。这个阶段是可中断的(特别是在并发模式下)。

它主要做两件事:

1. 处理更新队列,计算组件的新状态

React 会从 Fiber 节点的 updateQueue 中依次取出所有待处理的 Update,通过 processUpdateQueue 逐个应用,得到组件本次渲染的最终新状态。

对于多个 setCount(count + 1) 调用,它们会在一次渲染中被合并处理,但每个更新函数都会被依次调用,确保最终状态是基于前一个结果累积的(函数式更新)。而对象式更新(如 setState({a:1}))则可能被合并,所以需要小心闭包陷阱。

2. 执行组件函数,生成新的子 Fiber 树

  • React 重新调用你的函数组件(或者类组件的 render 方法),传入新的 Props 和计算出的新 State。
  • 函数返回新的虚拟 DOM(JSX 描述),React 将其转化为新的 Fiber 节点,与旧的 Fiber 节点进行比较(Diff)。
  • Diff 过程遵循我们讲过的同层对比、类型判断、列表 key 优化等规则。
  • 所有发现的变化(如需要插入、删除、更新 DOM 等)都被记录在 Fiber 节点的 flags(以前叫 effectTag)上,并构建出一个 Effect List(副作用链表),供 Commit 阶段使用。

重点:Render 阶段完全在内存中进行,不会触及真实 DOM。并且它是纯函数式的,外部不应在此期间有副作用。

6.4.5 阶段四:Commit——一次性操作真实 DOM 并执行副作用

Render 阶段结束后,React 获得了一个完整的副作用链表,然后进入不可中断的 Commit 阶段,将变化同步到环境(DOM)中。

Commit 阶段分为三个子阶段:

1. Before Mutation(突变前)

  • 操作真实 DOM 之前,调用 getSnapshotBeforeUpdate(类组件中)或执行一些需要在变化前读取旧布局信息的逻辑。
  • 此时 DOM 还是旧的,可以进行最后的测量。

2. Mutation(突变)

  • React 遍历 Effect List,依次执行 DOM 操作:新增、删除、替换、更新属性(如 classNamestylechecked 等)。
  • 这些操作直接修改真实 DOM,同步不可中断
  • 对于函数组件,关联的 useLayoutEffect 的清理函数会在此阶段同步执行(因为 useLayoutEffect 的清理阶段在 DOM 变更后、浏览器绘制前)。

3. Layout(布局)

  • 此时 DOM 已经更新完成,浏览器会计算新的布局(Layout)。
  • React 同步执行 useLayoutEffect 的回调函数。
  • 然后异步调度 useEffect 的回调(在下一次宏任务或微任务中执行)。

6.4.6 最后一步:浏览器绘制

严格来说这已不属于 React 内部流程,但用户看到画面更新的最后一步是:

  • React 在 Commit 阶段的 Mutation 子阶段改变了 DOM。
  • 浏览器在合适的时机(通常是下一帧)进行重绘(Paint),将新的像素展示在屏幕上。
  • 整个过程:JavaScript 执行 → React 渲染 → DOM 变更 → 浏览器布局 → 浏览器绘制

6.4.7 完整流程图示

触发状态更新 (setState / dispatch)
        │
        ▼
创建 Update 并加入更新队列
        │
        ▼
调度更新(Scheduler 根据优先级安排任务)
        │
        ▼
Render 阶段(可中断)
├─ 处理更新队列,计算新状态
├─ 调用组件函数,生成新 Fiber 树
└─ Diff 算法对比新旧 Fiber,标记变更 Flags
        │
        ▼
Commit 阶段(不可中断)
├─ Before Mutation
├─ Mutation (真实 DOM 操作)
└─ Layout (useLayoutEffect 同步执行)
        │
        ▼
异步调度 useEffect 清理与回调
        │
        ▼
浏览器绘制,用户看到更新后的 UI

6.4.8 一个实际例子串联整个流程

function Counter() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(1);
    setCount(prev => prev + 1);
  };

  return <button onClick={handleClick}>{count}</button>;
}

点击按钮时:

  1. 合成事件触发 handleClick
  2. 执行两个 setCount 调用,两个 Update 加入当前 Fiber 的 updateQueue
  3. 事件处理结束后,React 检查该 Fiber 有更新,调度一次同步渲染。
  4. Render 阶段:取出两个 Update,第一个 setCount(1) 将基础状态设为 1,第二个 prev => prev + 1 基于 1 得到 2。最终新状态为 2。执行 Counter() 返回 <button>2</button>。与旧 Fiber (<button>0</button>)比较,唯一的变更就是按钮文本从 "0" 变成 "2",标记文本更新 Flag。
  5. Commit 阶段:Mutation 子阶段更新 DOM 按钮的文本内容为 "2"。
  6. 浏览器绘制,用户看到按钮显示 "2"。

如果需要异步数据请求,背后的调度和批量更新逻辑会更复杂,但流程主干相同。


掌握了从触发到刷新的全链路,你就有了一个“上帝视角”——任何性能问题(如卡顿、重复渲染)都可以在这个链条上定位到具体环节。下一步我们将进入 Hooks 原理,看看这些 API 如何在这个流程中发挥作用的。