React 的 Diff 算法采用同层比较策略,而不是传统的全树递归对比。这一设计是 React 虚拟 DOM 高性能的关键所在。
策略核心:放弃跨层级移动假设
传统 diff 算法(如两棵树的最小编辑距离)的时间复杂度为 O(n³) ,对于复杂 UI 来说性能不可接受。React 基于一个大胆但实用的假设:在 UI 更新中,DOM 节点很少出现跨层级的移动。也就是说,一个 <div> 通常不会从页头“飞”到页脚,组件树的结构变化大多发生在同一层级内。
基于这个假设,React 的 Diff 算法只对同一层级的节点进行比较,一旦发现节点类型不同,直接卸载整个子树,不再向下递归比较。这使得算法的时间复杂度从 O(n³) 降为接近 O(n),极大提升了渲染性能。
深度优先、逐层对比
当你触发一次状态更新,React 会生成一棵新的虚拟 DOM 树,然后从根节点开始,对两棵树的对应层级进行深度优先的遍历对比:
- 根节点比较:先比较新旧两棵树的根节点类型。
- 向下递归:如果节点类型相同,继续递归比较它们的子节点,同样是按层级顺序。
- 停止跨层:如果发现某层节点的类型不同(例如从
<div>变为<span>),React 会认为该节点及其子节点都发生了变化,直接销毁旧节点及其子树,创建新节点及其子树,不再深入比较子节点。
旧树: 新树:
div div ← 类型相同,继续向下
├─ p ├─ span ← 类型不同!销毁 p 及其子节点,创建 span
└─ button └─ button ← 类型相同,继续向下
在上面的例子中,React 发现 <p> 变成了 <span>,会直接卸载整个旧的 <p> 子树,并用新的 <span> 子树替换,而不会深入比较 <p> 里面的文本是否相同。
同级子节点的处理:key 属性
对于同一层级的多个子节点(如列表),React 默认按顺序比较。如果列表顺序发生变化或者有新增/删除,仅靠顺序比较会导致大量不必要的重建。这时 key 属性就派上用场:
- 给每个子节点指定一个稳定、唯一的 key,React 就能根据 key 进行精确匹配。
- 即使节点在列表中的位置变了,只要 key 相同,React 就会认为它是同一个组件,只移动位置而不销毁重建。
// 优化前:顺序变化导致全部重建
<ul>
{items.map(item => <li>{item.text}</li>)}
</ul>
// 优化后:通过 key 实现精准复用
<ul>
{items.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
为什么是深度优先
React 选择深度优先遍历是出于实际 UI 场景的考虑:
- 界面通常有明确的层次结构(Header → Nav → Content),深度优先符合人的直觉。
- 配合同层比较,可以尽早发现差异并终止无意义的深层比较,节约计算资源。
- 通过 key 优化,在列表等场景中也能保持高效。
与传统 diff 的实际差异
传统通用树 diff 允许节点任意移动(跨层移动),因此需要复杂的计算。React 通过牺牲这种“理论上完美”的对比,换取了稳定且高效的渲染性能。在真实业务中,UI 的跨层移动极为罕见,而列表内的顺序变化则通过 key 高效处理。这一策略在前端领域被广泛验证,也是后续 Vue 等框架借鉴的核心设计。
总结:同层比较 + 深度优先 + key 优化,构成了 React Diff 算法的三大基石,让 React 在保持声明式开发体验的同时,依然具备出色的运行时性能。