Appearance
React 渲染、diff、Fiber 与更新过程
很多人提 React 原理时,最容易把这些词堆在一起:
- 虚拟 DOM
- diff
- Fiber
- 调和
- 渲染
但如果不把它们放回同一条主线上,就很容易只剩零散定义。
这篇文章重点讲:
- React 更新一轮 UI 时到底发生了什么
- 虚拟 DOM 解决的是什么问题
- diff 在比较什么
key为什么重要- Fiber 为什么不是“另一个虚拟 DOM”
1. React 更新 UI 的主线是什么
可以把 React 的一次更新压缩成 3 个阶段:
render:根据最新状态生成新的 React Element 树reconcile:把新旧树做比较,找出要变动的部分commit:把最终变更提交到真实宿主环境,例如浏览器 DOM
把这条主线拉开:
- React 不是一改 state 就立刻改 DOM
- React 会先算“新的 UI 应该长什么样”
- 再比较差异
- 最后统一提交
这条主线最适合用来串起后面的虚拟 DOM、diff 和 Fiber。
2. 虚拟 DOM 到底是什么
虚拟 DOM 不是浏览器里的真实 DOM。
它本质上是一种 JavaScript 层的 UI 描述模型,用来表达:
- 节点类型是什么
- 节点上有哪些属性
- 子节点结构是什么
例如:
jsx
function App() {
return (
<div className="page">
<h1>Hello</h1>
<button>Click</button>
</div>
);
}这段 JSX 经过编译后,最终会变成一棵描述型对象树,而不是直接变成真实 DOM。
🌟 虚拟 DOM 最准确的价值,不是“一定更快”,而是 让 UI 更新过程可以在 JavaScript 层计算和比较,再统一落到宿主环境。
3. 什么叫调和(Reconciliation)
调和可以看成 React 在新旧两棵 UI 描述树之间寻找差异,并决定哪些部分要复用、哪些部分要更新。
也就是说,调和回答的是:
- 哪些节点还是原来的
- 哪些节点需要替换
- 哪些节点只是属性变了
- 哪些子节点顺序变化了
diff 只是调和过程里最常被单独提出来的一部分。
4. React 的 diff 在比较什么
React 不会拿整棵树做最暴力的通用最优算法比较。
工程上它采用的是一套更可落地的启发式策略。
最值得先记住的是这几条:
4.1 不同类型的节点,通常直接认为不是同一个东西
例如:
jsx
<div />变成:
jsx
<section />React 更可能直接把它看成旧节点卸载、新节点挂载,而不是在原地“智能变形”。
4.2 同层比较非常重要
React 更强调同一层级下节点的比较,而不是跨层随意做最优迁移。
4.3 key 用来标识同层节点身份
这就是为什么列表渲染里 key 非常关键。
5. key 为什么会影响状态和性能
很多人以为 key 只是为了消除警告。
其实它的真正作用是,告诉 React,同层节点里谁是谁。
例如:
jsx
{
users.map(user => <UserRow key={user.id} user={user} />)
}这里的稳定 key 可以帮助 React:
- 更准确地复用节点
- 避免不必要的销毁和重建
- 避免列表项状态错位
反过来,如果你写成:
jsx
{
users.map((user, index) => <UserRow key={index} user={user} />)
}当列表发生插入、删除、重排时,就更容易出现:
- 输入框状态串位
- 动画错位
- 不必要的重新挂载
🌟 所以 key 最重要的价值不是“提高一点点性能”,而是 帮助 React 正确识别节点身份,保留该保留的状态。
6. 一段列表重排示例
下面这个例子最适合帮助理解 key 的意义。
jsx
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
function TodoItem({ todo }) {
const [editing, setEditing] = useState(false);
return (
<li>
<span>{todo.title}</span>
<button onClick={() => setEditing(prev => !prev)}>
{editing ? "完成编辑" : "编辑"}
</button>
</li>
);
}这里如果 key 稳定,列表重排时,React 更容易把“这条待办对应的本地编辑状态”保留下来。
如果 key 不稳定,React 可能把原本属于 A 行的状态误套到 B 行。
7. commit 阶段在做什么
很多人理解到 diff 就停了,但 React 还有 commit 阶段。
commit 可以看成 把前面计算好的变更真正应用到宿主环境。
在浏览器里,这一步通常包括:
- 创建、更新、删除 DOM 节点
- 绑定或解绑事件
- 执行布局相关副作用
- 处理 ref 挂载和卸载
这也是为什么很多 Hook 会围绕 commit 前后时机展开,例如:
useEffectuseLayoutEffect- ref 更新
8. Fiber 到底是什么
Fiber 不是“另一个虚拟 DOM”,也不是“一个新的 diff 算法名字”。
Fiber 是 React 内部的更新任务组织和调度模型。
它主要解决的是:
- 更新工作怎么拆分
- 哪些更新优先级更高
- 一次大更新能不能中断、恢复、继续
- 如何避免长时间阻塞主线程
在 Fiber 出现之前,你可以把 React 更新近似理解成“一次性做完一大块工作”。
Fiber 则把它变成了“可拆分、可调度、可中断的一系列小任务”。
9. 为什么 Fiber 很重要
Fiber 重要,不是因为它让 React 这个词更“底层”,而是因为很多现代 React 能力都建立在它之上。
例如:
- 更细粒度更新调度
- 更合理的任务优先级
- 并发相关能力
- 更平滑的用户交互响应
把这几个词放回各自位置看:
- 虚拟 DOM 更偏“描述 UI”
- diff 更偏“比较差异”
- Fiber 更偏“组织更新工作并调度执行”
10. 一张总图把关系串起来
mermaid
flowchart TD
A[state 或 props 变化] --> B[函数组件重新执行]
B --> C[生成新的 React Element 树]
C --> D[调和 reconcile]
D --> E[diff 新旧节点]
E --> F[生成变更列表]
F --> G[commit]
G --> H[更新 DOM 与副作用]
D --> I[Fiber 调度与任务拆分]这张图最重要的作用,是把 3 件容易混的事分开:
- UI 描述怎么生成
- 差异怎么比较
- 更新工作怎么调度
11. React 渲染原理里最常见的误区
11.1 以为虚拟 DOM 的核心结论就是“更快”
这太绝对了。
它更核心的价值是统一 UI 描述和更新模型。
11.2 以为 diff 是全量最优算法
不是。
React 用的是工程上更可控的启发式比较策略。
11.3 以为 Fiber 就是虚拟 DOM 2.0
也不对。
Fiber 的重点是更新任务的拆分与调度。
11.4 以为 key 只是为了警告消失
它真正影响的是节点身份和状态保留。
12. 一句话总结
React 的渲染原理主线,本质上是 先用 React Element 描述 UI,再通过调和和 diff 找出变化,最后在 commit 阶段更新真实环境,而 Fiber 负责把这套更新工作拆分并调度起来。