人人都会AI编程

列表 Diff:双端对比算法、key 的核心作用与误用后果

更新时间:2026-07-11

当 React 对同一层级的子节点列表进行 Diff 时,由于子节点的数量可能发生变化或顺序被重新排列,仅按位置逐项对比会导致大量无效的 DOM 更新。React 针对列表场景采用双端对比算法,并通过 key 属性 来标记每个列表项的唯一身份,从而以 O(n) 的复杂度高效完成新旧列表的差异计算。

双端对比算法的工作流程

双端对比算法的核心思想是:同时从新旧列表的两端向中间进行对比,尽可能减少节点的移动操作。假设有新旧两个子节点数组 oldChildrennewChildren,算法会维护四个指针:

  • oldStartIdx(旧列表起始索引)
  • oldEndIdx(旧列表结束索引)
  • newStartIdx(新列表起始索引)
  • newEndIdx(新列表结束索引)

每次循环会进行以下四步判断,如果某一步匹配成功则移动指针并继续循环:

  1. 旧首 vs 新首:如果 oldStartnewStart 的节点类型相同且 key 相同,则进行深度对比并更新属性,两个指针同时后移。
  2. 旧尾 vs 新尾:如果 oldEndnewEnd 的节点匹配,则对比更新后两个指针同时前移。
  3. 旧首 vs 新尾:如果 oldStartnewEnd 匹配,说明旧列表的首节点被移动到了新列表的末尾,此时在真实 DOM 中将该节点移动到 oldEnd 之后,然后旧首指针后移、新尾指针前移。
  4. 旧尾 vs 新首:如果 oldEndnewStart 匹配,说明旧列表的尾节点被移动到了新列表的首部,将节点移动到 oldStart 之前,然后旧尾指针前移、新首指针后移。

如果以上四种情况都不匹配,算法会尝试在旧列表中查找与新首节点 key 相同的节点。找到则将该节点移动到 oldStart 之前,更新其属性;找不到则视为全新节点,创建并插入到 oldStart 之前。然后新首指针后移。

当某一列表的起始指针超过结束指针时,说明该列表已处理完毕:若旧列表还有剩余节点(oldStartIdx <= oldEndIdx),这些节点需要从 DOM 中删除;若新列表还有剩余节点(newStartIdx <= newEndIdx),这些节点需要新创建并插入。

这种策略在常见的列表操作(尾部追加、头部插入、中间插入/删除、简单倒序)中都能以最小的移动次数完成更新,避免了对整个列表的全量重建。

key 的核心作用

key 是 React 识别每个列表项唯一性的唯一标识。在列表 Diff 过程中,React 使用 key 来判定两个节点是否“相同”,而不是依赖其在列表中的位置。这带来两个核心收益:

  1. 精准的节点复用与状态保持

当列表顺序改变时,React 根据 key 找到对应的旧 DOM 节点,将其移动到正确位置,而不是销毁再重建。这不仅能避免无谓的 DOM 操作,还能保持节点的内部状态(如输入框内容、组件内部的 useState 值),因为同一个 key 的组件实例会被保留。

  1. 避免全量重建的性能陷阱

如果没有 key 或使用错误的 key,React 会退化为按索引逐一对比。当列表在头部插入一项时,所有后续项的位置都发生了变化,React 会认为它们全部需要更新,导致整个列表被销毁并重新创建,造成严重的性能浪费和状态丢失。

正确用法:key 应该是一个在兄弟节点间稳定、唯一且可预测的标识符,通常使用数据中的 ID(如 user.id)。只有当列表项在所有渲染中都是固定不变且永不重新排序时,才可以使用索引作为 key,但即便此时也应谨慎。

// ✅ 推荐
{users.map(user => <UserCard key={user.id} user={user} />)}

// ⚠️ 仅当列表静态不变时可接受,否则危险
{todos.map((todo, index) => <TodoItem key={index} todo={todo} />)}

key 的误用后果

  1. 使用索引作为 key

当列表发生插入、删除或排序时,索引会发生变化。例如在列表头部插入一项,原来第 1 项的索引变成 2,第 2 项变成 3,导致 React 错误地将旧的 DOM 节点与新位置的数据绑定,产生界面错乱、输入状态错位。你可以用一个简单实验验证:生成一个包含多个输入框的列表,用索引作为 key,在头部插入一行后,观察输入框内容是否还对应原来的项——大多数情况会发生错位。

  1. 使用随机值(如 Math.random())作为 key

每次渲染时都会生成新的 key,导致 React 认为所有列表项都是全新的,进而销毁所有旧节点并重建新节点。这不仅带来巨大的性能开销,还会丢失所有组件内部状态(如折叠展开、动画进度等),还可能引发不必要的副作用重复执行。

  1. key 不唯一或缺失

如果有两个节点使用了相同的 key,React 在 Diff 时会混淆,导致不可预测的渲染结果和状态混乱。如果完全不写 key,React 会在控制台给出警告,并退化为使用索引,隐含着上文所述的风险。

  1. 将 key 放在组件的属性而非最外层元素上

key 必须设置在最外层被遍历的直接子元素上,而不是组件内部的根元素上。例如:

   // ❌ 错误:key 是 React 保留属性,在组件内部无法读取
   function MyComponent(props) {
     return <div key={props.id}>...</div>;   // 这里的 key 无效
   }
   // ✅ 正确
   <MyComponent key={item.id} />
   

实际开发中的建议

  • 始终为列表项选择一个稳定且唯一的 key(优先数据库 ID 或业务主键)。
  • 如果数据确实没有唯一 ID,可以结合数据内容生成哈希值,或创建新的唯一标识(如使用 crypto.randomUUID() 在数据创建时固化,而非渲染时生成)。
  • 在代码审查中,将“未指定 key 或使用索引作为 key”视为需要修复的问题,除非列表已被明确标记为静态不可变。
  • 理解 key 的背后机制,能帮助你快速定位列表相关的界面异常:当发现状态错乱或无故重新渲染时,首先检查 key 的设置是否正确。