diff
React 中的 diff 算法 是虚拟 DOM (Virtual DOM) 更新过程中的核心优化机制,用于高效比较新旧虚拟 DOM 树的差异,并仅将必要的变化同步到真实的 DOM 中。解决了传统虚拟 DOM 全量比较的性能瓶颈,是 React 高效渲染的关键。
这个算法并不是传统意义上的“完整树 diff”,而是一种 启发式 、 优化过的增量对比算法 ,目的是以尽可能低的时间复杂度找出需要更新的部分,从而高效地更新真实 DOM。
一、diff 算法的基本概念
在传统前端渲染中,直接操纵真实 DOM 成本很高(涉及浏览器重排/重绘)。因此 React 提出 虚拟 DOM : 用 JS 对象模拟真实 DOM 结构,当状态变化时,生成新的虚拟 DOM 树,与旧的树对比 ( diff ),找出差异后仅更新真实 DOM 中变化的部分。
在计算机科学中, Diff(Difference)算法 通常指用于比较两个数据结构(如文本、树等)之间差异的算法。例如 Git 使用 diff 来比较代码变更。
但直接比较两棵树的差异复杂度极高(传统树对比是 O(n³) 时间复杂度)。React 基于 启发式规则 对 diff 过程做了大量优化,将复杂度降低到 O(n) ( n 为节点数量)。
二、 React Diff 算法的核心规则
React 的 diff 算法通过以下三个策略大幅降低计算量:
1. 只对比同级节点(Tree Diff)
React 假设 不同层级的节点不会跨层级移动 。因此,diff 只会在同一层级节点间进行,不会跨层级对比。
- 若某个节点在新旧树中层级不同(如从
<div>下移动到<span>下),React 会直接销毁旧节点及其所有子节点,然后在目标位置重新创建新节点 - 这一策略将树对比简化为多层级独立的树对比,复杂度从
O(n³)降至O(n²)(n 为每层节点数)
2.通过 key标识稳定节点(Element Diff)
对于同一层级的多个子节点(如列表),React 要求为每个节点添加唯一 key,用于标识节点的 稳定性 (即节点在多次渲染中的身份是否不变)。
- 若没有
key,React 会默认按顺序对比,当列表顺序变化时(如插入/删除中间项),可能导致大量节点被错误销毁或更新 - 有
key时,React 通过key匹配新旧节点:- 匹配成功:复用节点,仅更新变化的属性
- 匹配失败:销毁旧节点,创建新节点
- 未被匹配的新节点:创建
- 未被匹配的旧节点:销毁
这一策略将子节点对比的复杂度从 O(n²) 降至 O(n)(依赖 key 的高效查找)
3. 组件类型决定复用性(Component Diff)
对于自定义组件,React 会比较组件类型:
- 若类型不同(如
<div>→<p>或ComponentA→ComponentB):直接销毁旧组件,创建新组件 - 若类型相同:复用组件实例,仅更新
props,触发组件的shouldComponentUpdate或React.memo优化逻辑,避免不必要的渲染
| 方法 | 时间复杂度 | 说明 |
|---|---|---|
| 传统树 | O(n³) | 需要尝试所有可能的节点映射,成本极高 |
| React 启发式 diff | O(n) | 基于上面的三个假设,线性遍历即可 |
三、Fiber 架构下 diff 算法的升级
React 16 引入 Fiber 架构后,diff 算法与 协调过程(Reconciliation) 深度结合,进一步优化了性能和灵活性。
1. 可中断的增量 diff
Fiber 将整个 diff 过程拆分为多个 小任务单元 (Fiber 节点),每个任务执行完一小段时间后会暂停,将控制权交还给浏览器(通过 requestIdleCallback或 requestAnimationFrame),避免长时间阻塞主线程。
- diff 不再是 “一次性全量计算”,而是 按需分片执行 ,提升了应用的响应速度(尤其是长列表或复杂组件树)
2. 双缓存树(Double Buffering)
Fiber 维护两棵虚拟 DOM 树:current(当前屏幕显示的树)和 workInProgress(正在计算的下一帧树)。
- diff 过程基于
workInProgress树进行,完成后直接替换current树 - 双缓存避免了重复创建树的开销,且
workInProgress树的节点可直接复用current树的节点(通过alternate属性),减少内存分配和 GC 压力
3. 更细粒度的节点标记
Fiber 为每个节点标记 副作用( Effect ) ,如 Placement(插入)、 Update(更新)、 Deletion (删除)。
- diff 完成后,React 仅需遍历带有副作用的节点,批量更新真实 DOM,进一步减少操作次数
Fiber 并没有改变 diff 算法的核心逻辑,只是让 diff 过程变得更可控、更响应
四、React diff 算法的优势
- 性能高效 :通过同级对比、key标识和组件类型判断,将树对比复杂度从
O(n³)降至O(n),大幅提升渲染效率 - 可中断与并发 :Fiber 架构下的分片 diff 支持任务中断和恢复,避免阻塞主线程,提升应用流畅度(尤其是复杂场景)
- 灵活的更新控制 :通过
key、组件类型和副作用标记,精准定位变更,减少不必要的 DOM 操作 - 内存优化 :双缓存树减少了内存分配和垃圾回收的开销,长期运行更稳定
- 支持组件级复用 : 对于自定义组件, Diff 会对比组件的
type和key,若相同
五、避坑
- 不要使用索引作为列表的 Key :列表增删、排序时,索引会变化,导致 React 误判 “节点已替换”,触发不必要的 DOM 重建(比如列表删除第一个元素,后面的元素 key 从 1→0,React 会认为是 “新节点”)
- key 必须为一切稳定 : 同一层级的节点 key 不能重复,且不要用随机值(每次渲染生成新 key,会导致节点无法复用,全量重建)
- 跨层级移动节点要慎重 : Diff 不跨层对比,若频繁将节点从 A 层级移到 B 层级,会导致旧节点销毁、新节点创建,性能较差(尽量通过 CSS 或组件结构优化避免跨层级移动)