React 的 Diff 算法(协调算法)是虚拟 DOM 实现高性能更新的关键。它的目标是在新旧两棵虚拟 DOM 树之间,以最小的操作代价完成真实 DOM 的更新。React 基于两个假设和三大策略,将传统 O(n³) 的树比较问题优化到接近 O(n) 的复杂度。
两个核心假设
- 不同类型的元素会产生不同的树:如果元素的类型(如
div变为span)发生变化,React 会直接销毁旧的子树,重新创建新的子树,不再进行深度比较。 - 开发者可以通过
key来标识哪些子元素在不同渲染间是稳定的:在列表渲染中,key帮助 React 识别哪些项只是移动了位置,哪些是新增或删除。
三大核心策略
1. 同层比较(Tree Diff)
React 不会跨层级比较节点,而是只比较同层级的节点。如果某个节点在旧树中位于第 2 层,在新树中变成了第 3 层,React 会直接删除该节点并重新创建它,而不会尝试将其移动到新位置。
这种分层对比策略牺牲了理论上“最优移动”的可能,但极大地降低了算法复杂度,且在实际 UI 中,跨层级移动 DOM 的情况极少发生。
2. 节点类型比较(Component Diff)
React 在比较两个节点时,首先检查它们的 type:
- 相同类型:React 保留该节点,仅更新变化的属性(Props),然后继续递归比较子节点。
- 不同类型:React 会彻底销毁旧节点(包括其下所有子树),然后创建新节点。例如
<div>变成<span>,旧的div及内部全部 DOM 会被移除,新的span会被构建并插入。这很符合直觉——一个按钮和一个输入框不可能通过微调属性就互相转换。
对于 React 组件节点(函数或类),React 的行为稍有不同:
- 如果同一位置的组件类型相同,React 会保留组件实例(或保持其内部状态),调用
render并比较返回的虚拟 DOM(对函数组件,重新执行后比较返回值),然后递归进行子节点 Diff。 - 如果组件类型不同,React 会卸载旧组件,挂载新组件,所有状态都会丢失。
3. 列表比较(List Diff)
对同一层级的子节点进行 Diff 时,React 默认按顺序逐个比较。但在实际场景中,子节点往往是一个列表,可能会发生顺序变化、新增或删除。React 采用双端对比算法(或称“从左向右+从右向左”算法),并结合 key 来优化。
算法流程(简化):
- 设置四个指针:旧列表头尾、新列表头尾。
- 同时进行四向比较:旧头 vs 新头、旧尾 vs 新尾、旧头 vs 新尾、旧尾 vs 新头,找到可以复用的节点。
- 如果某节点有
key,优先通过key匹配复用;如果无法复用,则创建新节点;旧列表中多余的节点会被删除。 - 处理完所有匹配后,剩下没有处理的节点就是需要新增或移动的。
示例:假设列表从 [A, B, C] 变为 [B, A, C],没有 key 时:
- 默认逐个比较:第一个位置 A vs B,类型可能不同导致 A 被替换;第二位置 B vs A 又替换……产生大量重建。这还可能导致不必要的状态丢失(比如输入框内容)。
- 有
key="A"、key="B"、key="C"时:React 能识别出 A 和 B 只是换了位置,仅通过移动 DOM 节点即可,性能更优,且组件状态得以保留。
4. Key 的核心作用与误用后果
key 是 React 用来识别列表中每个元素的唯一且稳定的标识。它帮助 React 判断哪些元素可以复用,哪些需要重建。
最佳实践:
- 使用数据中的唯一 ID(如
user.id)。 - 如果不具备,可以使用其他稳定且唯一的字段组合。
- 只有在列表是静态且不会重排序时,才可考虑使用索引
index。
误用后果:
- 使用数组索引
index作为 key:如果列表发生插入、删除或排序,索引会变化,导致 React 误判元素身份。例如删除第一项后,旧索引0的元素可能错误地复用了旧索引1的组件状态,引发界面错乱(输入框内容跑到其他项上)。 - 使用不稳定的随机值(如
Math.random()):每次渲染 key 都变,React 会认为元素全变了,导致大量重建,性能极差。 - 缺少 key 或重复 key:React 会使用索引作为 fallback,同样面临上述问题,并发出警告。
正确的 key 能保证列表 Diff 以最小的 DOM 操作完成,同时保持组件状态与 UI 的正确绑定。这是 React 列表性能优化中最简单也最重要的一步。