Reconciliation 算法(Diff)
Reconciliation(协调)是 React 对比上一棵 Fiber 树与本次 render 产出,决定哪些 DOM 需要插入、更新或删除的过程。实现主要在 ReactChildFiber.js(reconcileChildrenArray 等)。
1. 要解决什么
| 目标 | 做法 |
|---|---|
| 少碰 DOM | 只在 Commit 的 Mutation 阶段批量改 DOM |
| 快 | 用启发式 Diff,避免 O(n³) 树编辑距离 |
| 可中断 | Diff 在 Render 阶段完成,结果挂在 Fiber.flags 上 |
Virtual DOM 不是目的,可预测的更新路径才是。
2. 两条假设(算法前提)
- 不同类型 → 不同子树:根类型变了(如
div→span),直接拆掉旧子树、建新子树,不再深入对比。 - 同层、同类型用 key 认人:列表重排时靠 key 复用实例,而不是按索引硬比。
因此:不要跨层级移动节点(极少 UI 会这样),列表务必 稳定 key。
3. 同层比较在比什么
对同一父节点下的一串子 Fiber,大致维护 lastPlacedIndex(已处理子节点在旧列表中的最右位置):
| 情况 | 行为 |
|---|---|
| key 相同、类型相同 | Update,复用 DOM 节点,改 props |
| key 不同或新节点 | Placement,插入新 DOM |
| 旧列表有、新列表没有 | Deletion,删除 DOM |
移动:新节点在旧列表中的 index 小于 lastPlacedIndex → 需要移动 DOM(标记 Placement)。
源码用 flags:Placement | Update | ChildDeletion(见 02-Fiber §5)。
4. key 实践
| ✅ | ❌ |
|---|---|
| 业务 id | 数组 index(可重排列表) |
| 稳定、唯一 | Math.random() 作 key |
index 作 key 会导致错误复用、输入框状态错位、动画异常。
5. 与 bailout 的关系
若子树 props/state 未变,Reconciler 可 bailout(跳过子节点 Diff)。这与 Diff 本身正交:Diff 解决「要比」,bailout 解决「要不要比」。
6. 流程简图
mermaid
flowchart TD
A[render 产出新 React Element] --> B[reconcileChildren]
B --> C{同层逐项}
C -->|同 key 同 type| D[Update Fiber]
C -->|新| E[Placement]
C -->|缺| F[Deletion]
D --> G[flags 挂到 Fiber]
E --> G
F --> G
G --> H[Commit Mutation 写 DOM]7. 阅读路径
| 文件 | 内容 |
|---|---|
ReactChildFiber.js | 单节点、数组 reconcile |
ReactFiberBeginWork.js | 进入 reconcile 的时机 |
ReactFiberCommitWork.js | 根据 flags 操作 DOM |
8. 总结
- 只比同层,类型不同即换树。
- key 决定列表复用与移动成本。
- 结果落在 Fiber.flags,Commit 再统一改 DOM。
工程上:稳定 key + 避免在 render 里乱改结构,比背源码更重要。更多 FAQ 见 09-常见问题。