人人都会AI编程

5.3 Diff 算法核心策略

更新时间:2026-07-10

React 的 Diff 算法(协调算法)是虚拟 DOM 实现高性能更新的关键。它的目标是在新旧两棵虚拟 DOM 树之间,以最小的操作代价完成真实 DOM 的更新。React 基于两个假设和三大策略,将传统 O(n³) 的树比较问题优化到接近 O(n) 的复杂度。

两个核心假设

  1. 不同类型的元素会产生不同的树:如果元素的类型(如 div 变为 span)发生变化,React 会直接销毁旧的子树,重新创建新的子树,不再进行深度比较。
  2. 开发者可以通过 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 来优化。

算法流程(简化)

  1. 设置四个指针:旧列表头尾、新列表头尾。
  2. 同时进行四向比较:旧头 vs 新头、旧尾 vs 新尾、旧头 vs 新尾、旧尾 vs 新头,找到可以复用的节点。
  3. 如果某节点有 key,优先通过 key 匹配复用;如果无法复用,则创建新节点;旧列表中多余的节点会被删除。
  4. 处理完所有匹配后,剩下没有处理的节点就是需要新增或移动的。

示例:假设列表从 [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 列表性能优化中最简单也最重要的一步。