🧠 React diff
React 框架的核心目标是在最小 DOM 更新成本下,让 UI 和最新状态保持一致。 由于 DOM 操作非常昂贵,如果每次更新都全量重绘,那性能会炸掉。 所以 React 在虚拟 DOM(vDOM)层做对比:新旧两棵树做差异计算(diff) → 最小代价更新真实 DOM。
但树的最优 diff 是 O(n³) ,不可接受。
React 把这个问题降为 O(n) —— 用一套启发式策略。
这就是“diff 机制”的来源。
在 Fiber 之前是 深度优先遍历(DFS)。 Fiber 之后依旧是深度优先,只是基于链表结构执行。
🧩 React Diff 的三大核心策略
策略 1:不同类型组件,直接卸载并重建(type 变就全重建)
<div /> → <span />
当组件的 type 改变后,React 不会继续向下深比较,而是:
1️⃣ 卸载旧节点(包括生命周期 componentWillUnmount)
2️⃣ 创建新节点
3️⃣ 渲染
这么做的原因可以总结为以下几点:
- 不同类型的组件结构差异大,深比较成本不值得
- 类型是一种非常稳定的区分方式
- 提升 diff 的确定性和速度
React 不会跨类型 diff
策略 2:同一类型节点只比较 props & children
<div className="a"></div> → <div className="b"></div>
当节点类型相同时,React 会按照以下步骤处理:
1️⃣ keep 节点不动
2️⃣ 对比属性差异
3️⃣ 再看 children
这是 vDOM diff 的核心加速点:同类型节点 → 只做局部 diff,不重建结构。
策略 3:列表 diff 必须依赖 key 来定位元素
[ A, B, C ] → [ B, C, A ]
如果 key 写得好(如数据库 ID),React 会聪明地:
- B → 保留
- C → 保留
- A → 移到最后
- 不会重建组件,不会丢 state
但如果你写 key={index}…
React 只能按索引位置猜测重用,会导致:
- 组件状态错位
- 多余重渲染
- 动画/输入丢失
key 代表节点的身份,必须是一个稳定的值,而 index 代表的是在列表中的位置,不是身份。当顺序变动时,会导致错误 diff、错误重用
🧵 Diff 工作流程
🔹 1. 节点层(Element Diff)
对比规则:
- type 相同 → 复用 DOM/组件实例 → 比较 props
- type 不同 → 直接替换
- 对比 ref 是否变化
- 最后去 diff children
流程非常稳定且确定。
🔹 2. 子节点层(Children Diff)
React 针对 children 类型不同,采用不同策略:
文本节点
直接比较字符串是否相同,不同则替换 textContent。
② 单节点
直接按第一条策略走:type 相同则深入 diff。
③ 多节点(列表 diff)
React 会:
- 第一遍扫描:确定 “老节点哪些能复用”
- 第二步:解决“移动”和“新增”
- 最后:删除不再存在的节点
React 做了优化:
- 按 key 建 Map,加速定位
- 头尾双端比较(快速处理常见场景:新增、删除、尾插)
假设有一个列表 A → B → C,经过更新后变成 B → C → D,React 处理的步骤如下:
| 操作 | 说明 |
|---|---|
| 删除 | A 不在新列表中,会被销毁 |
| 移动 | B、C 的 key 相同,但位置变化,React 会移动 DOM 节点 |
| 添加 | D 是新节点,会创建新的 DOM |
💡 3. 头尾双端比较
当列表节点数量比较大时,React 内部优化了 diff 算法,尤其是 移动、插入、删除的场景。核心是:
-
同时从头和尾进行比较
-
新旧列表各有两个指针:
oldStart、oldEnd、newStart、newEnd -
步骤:
- 比较
oldStart与newStart - 比较
oldEnd与newEnd - 比较
oldStart与newEnd(头移尾) - 比较
oldEnd与newStart(尾移头)
- 比较
-
一旦匹配成功,就移动指针继续向内推进。
-
-
核心思路:
-
前后双向扫描:快速找到头尾未移动的节点,减少中间查找次数
-
处理剩余未匹配节点:
- 新节点未匹配 → 创建
- 老节点未匹配 → 删除
- 中间节点可能移动 → 使用 key 映射表快速定位
-
-
时间复杂度优化:
- 传统 diff 是 O(n²)(每个新节点找老节点)
- 双端 + key 映射 → O(n),这是 Vue 3 和 React Fiber 中 diff 的优化思路。
举个简单例子
旧列表:[A, B, C, D]
新列表:[B, A, E, D]
-
步骤:
-
oldStart=A, newStart=B→ 不匹配 → 尝试oldEnd=D, newEnd=D→ 匹配,D不动 -
oldStart=A, newStart=B, oldEnd=C, newEnd=E→ 头尾扫描都不匹配 -
建立 key 索引表:
{A:0, B:1, C:2, D:3} -
扫描新列表:
B→ 在老列表 index=1 → 移动到新位置A→ index=0 → 移动E→ 不在老列表 → 创建
-
-
剩余
C→ 不在新列表 → 删除
最终操作:移动 B 和 A,删除 C,创建 E。
⭐ 4. 节点处理
React 通过 key 来判断节点的“身份”,并且记录其旧位置和新位置。如果一个节点的新位置比之前小,就会标记为“需要移动”。不过 React 不会真的立刻移动 DOM,它只是:
- 记一个 “Placement” 副作用 tag
- 等到 commit 阶段一次性操作 DOM
🚀 总结
- “React diff 是 O(n) 的启发式算法,不追求最优解,而追求高效可预测。”
- “核心是 type 比较 + children diff + key 定位。”
- “diff 和 Fiber 紧密结合,前者负责找差异,后者提供调度能力。”
- “key 决定节点身份,是正确复用的根基。”