一句话定义
React 的工作流是三步循环:触发(Trigger)→ 渲染(Render,构建新虚拟 DOM 并 diff)→ 提交(Commit,把 diff 应用到真实 DOM);这个「计算差异」的过程叫协调(Reconciliation),其执行架构叫 Fiber。
为什么重要
这是把 React 从「会用」推向「会想」的一篇。性能问题(kp-019)、key 机制(kp-009)、并发模型(kp-022)、记忆化的适用边界(kp-018),全部以这个模型为解释基础。
前置知识
- kp-005 快照与 setter。
- 树结构的基本概念。
核心概念
- 虚拟 DOM(Virtual DOM):组件函数返回的 React 元素对象树,是真实 DOM 的轻量描述。
- 协调 / Diff:新旧两棵虚拟 DOM 树的对比算法。
- Fiber:React 内部的工作单元数据结构,使渲染过程可暂停、可恢复、可分优先级(React 16 重写引入,18 起全面兑现为并发能力)。
- 触发条件:组件自身 state 变化、父组件重渲染导致其重渲染、context 值变化。
- 提交:diff 结果一次性、同步地写入真实 DOM;之后执行 layout effects → 浏览器绘制 → passive effects(useEffect)。
原理 / 机制
三阶段全景图:
text[触发] setCount / 父组件重渲染 / context 变化
↓ 调度(Scheduler)给这次更新分配优先级(kp-022)
[渲染阶段 Render Phase] —— 可中断、可丢弃
自顶向下递归调用组件函数 → 产出新元素树
与上一棵树 diff:同 type? 复用 DOM/子树 : 重建
↓ 得到"最小变更集"
[提交阶段 Commit Phase] —— 同步、不可中断
写入真实 DOM → layout effects → 浏览器绘制 → useEffect
Diff 算法的三条简化假设(也是三条规则):
- 元素类型不同 → 整棵子树销毁重建。
<div>换成<span>,内部所有子元素全部重来,子组件 state 清零。 - 类型相同 → 复用 DOM 节点,只更新变化的属性/文本,并递归比较子节点。
- 同层列表按 key 匹配(kp-009)。
由假设 1 推出一个隐蔽 bug:
jsx{mode === 'input'
? <input placeholder="A" />
: <input placeholder="B" />}
两者都是 <input>,DOM 复用,焦点不丢——通常正合适。但若希望切换时重置,需要 key 强制区分:
jsx<input key={mode} placeholder={mode === 'input' ? 'A' : 'B'} />
「渲染阶段可中断」是 Fiber 的核心价值:一次大更新的渲染被切成许多 Fiber 工作单元,高优先级任务(用户输入)可以插队,先提交紧急更新,稍后回来继续慢更新。这也解释了为什么组件必须纯函数(kp-003)——渲染随时可能重头来。
直观类比
虚拟 DOM 像装修图纸:改家具不需要砸墙重来,比较新旧图纸(diff)、只派工人动该动的房间(commit)。Fiber 像可暂停的施工队:老架构(Stack reconciler)一开工必须从头干到底,用户觉得卡;Fiber 把工程拆成无数小任务单,接到紧急电话(输入事件)可以先放下瓦刀去开门,回来接着干。
实例 / 案例
用渲染模型解释三个常见现象:
jsx// 1. "点击没反应":父组件重渲染但子组件收到的 props 没变
// → 子组件仍会重渲染(默认行为),这是 kp-019 的优化对象
// 2. "状态莫名重置":条件分支改变了元素类型 → 子树重建 → state 清零
{showChart ? <ChartPanel /> : <TablePanel />}
// ChartPanel/TablePanel 即使内容相似,也是不同组件 → 各自独立 state
// 3. "commit 为什么同步":保证浏览器看到的是完整一致的一帧 UI
React DevTools Profiler 能按一次 commit 展示「哪些组件渲染了、为什么、耗时多少」——调试性能的第一个入口。
常见误区
- 「虚拟 DOM 快是因为 diff 快」:更准确的说法是它让 React 可以用 JS 对象计算最小变更,避免了手动 DOM 操作的低效与遗漏;真正的性能杠杆在「少渲染」与「正确复用」,不是「diff 有魔法」。
- 以为 state 变化会「精确地只重渲染相关组件」:默认情况下,父组件重渲染会连带所有子组件重渲染(除非 memo,kp-018/019)。
- 把渲染阶段副作用当可靠:渲染可能被丢弃重来;任何「只想执行一次」的逻辑放 effect 或事件。
- 混淆渲染与提交:
useLayoutEffect在提交后、绘制前;useEffect在绘制后。布局抖动问题的根源常在于选错(kp-010)。
自测题
- 写出三阶段流程,并标注哪一阶段可中断、为什么可中断(组件纯函数)。
<div><p>a</p></div>改为<span><p>a</p></span>,p 的 DOM 会复用吗?为什么?- 同一个组件在两个分支里渲染,切换分支时 state 保留还是清零?如果两个分支用的是同一个组件(
{flag ? <Panel mode="a"/> : <Panel mode="b"/>})呢?(保留——元素类型相同;加不同 key 才会重置。) - 为什么「渲染阶段可中断」在 React 16 之前不可能实现?
与其他知识点的关系
- 向前:是 kp-005 setter 之后故事的后半段。
- 向后:kp-009 深入第三条 diff 假设;kp-019 把模型变成性能排查手册;kp-018 的 memo/useMemo 全部作用在渲染阶段;kp-022 展开 Fiber 调度与优先级。
延伸阅读
- Render and Commit(官方三阶段):<https://react.dev/learn/render-and-commit>
- Fiber 架构笔记(A. Clark):<https://github.com/acdlite/react-fiber-architecture>