React 列表渲染必须添加 Key 的核心原理与最佳实践

作者:交互手记

时间:2026-10-25

浏览:611

本文深入解析 React 列表渲染中 Key 属性的必要性。通过剖析 React 的 DOM Diff 算法,说明 Key 如何帮助 React 识别节点身份,避免不必要的组件销毁与重建。文章详细讨论了不使用 Key 或错误使用索引作为 Key 时可能导致的内部状态丢失及 React.memo 缓存失效问题,并提供了基于唯一 ID 或生成器的最佳实践方案。

文章目录

在 React 开发中,渲染列表是常见需求,例如展示好友列表。

通常的做法是利用 Array.prototype.map 方法,将数组数据映射为 JSX.Element 数组,并将其嵌入到组件返回的 JSX 结构中,具体示例如下:

function FriendList() {
  const [items, setItems] = useState(['本站', '小明', '张三']);
  return (
    <ul>
      {items.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

必须为列表中的每一项提供 key 属性以作为唯一标识,从而区分不同的列表项。若省略 key,React 会抛出警告信息:

Warning: Each child in a list should have a unique "key" prop.

为什么需要 key?

在解答此疑问前,需简要了解 React 的 DOM Diff 算法机制。

当应用状态发生变更时,React 会对真实 DOM 树进行按需批量更新,从而生成新的用户界面。

底层实现上,React 会对新旧两棵虚拟 DOM 树执行 diff 对比,计算出差异补丁(patch),随后将这些补丁应用到真实 DOM 树上。

为了提升对比效率,React 的 diff 算法遵循以下限制:

  • 仅进行同层级节点的对比,不进行跨层级的比较。
  • 若元素类型不同(例如从 p 标签变为 div 标签),则视为不同节点,系统会销毁整个旧子树并调用其组件的卸载钩子,随后创建全新子树,这一过程性能消耗较大。
  • 若类型相同,则执行打补丁操作,例如更新 className 或标签内的文本内容。

然而,这种做法存在一个问题:当同级多个节点仅发生位置交换时,由于相同索引位置的节点无法对应,且发现无法复用,系统会销毁整棵树并重新创建,效率极低。

为此,React 允许开发者通过 key 标记节点,以优化 Diff 算法。Key 告知 React 该节点并未被移除或不能原地复用,只是改变了位置,从而让 React 仅更新其位置信息。

列表渲染不提供 key 会怎样?

若不提供 key,React 无法确定节点是否发生了移动。

此时,React 仅对比相同索引位置的两个节点。若它们类型相同(例如均为 li 元素),则比较 props 的差异并执行 props 的打补丁操作。

由于列表渲染通常涉及相同类型的元素,位置变动时大多会触发节点的原地复用,因此通常无需担心整棵树的销毁与重建。

在缺少 key 的情况下,原地复用有时仍能正确渲染列表。

但存在一种例外情况:若节点包含内部状态,最典型的例子即为输入框。

function FriendList() {
  const [items, setItems] = useState(['本站', '小明', '张三']);
  const swap = () => {
    [items[0], items[1]] = [items[1], items[0]];
    setItems([...items]);
  };
  return (
    <div>
      <ul>
        {items.map((item) => (
          <li>{item}<input /></li>
        ))}
      </ul>
      <button onClick={() => { swap(); }}>
        交换
      </button>
    </div>
  );
}

假设我们向第一个和第二个输入框分别输入内容。

为什么需要 key?

随后点击“交换”按钮,交换数组中第一个和第二个元素的位置。

此时观察到 input 标签前的文字已正确交换,但输入框内的内容并未随之交换。

为什么需要 key?

原因在于 React 执行了原地复用,且由于 input 未接收新的 props,无需执行 props 补丁,因此保持了原有状态。

解决该问题的方法是添加 key。通过 key 告知 React 节点需要移动,代码应修改为:

items.map((item) => (
  <li key={item}>{item}<input /></li>
))

不使用 key 的另一个劣势在于:原地复用会导致传入的 props 发生变化,进而无法充分利用 React.memo 的组件缓存能力。

列表渲染的 key 用数组索引会怎样?

其效果等同于不使用 key,依然基于新旧节点相同索引位置进行对比,但控制台不再打印警告信息。

应该用什么值作为 key?

对于列表节点,应使用唯一的 id 赋值给 key,通常源自数组数据的 id,例如后端返回的好友列表中的好友 id。

const [items, setItems] = useState([
  { id: 5, name: '本站' },
  { id: 9, name: '小明' },
  { id: 87, name: '张三' },
  { id: 91, name: '本站' }
]);
const list = items.map((item) => (
  <li key={item.id}>{item.name}</li>
));

若后端未返回 id,可手动使用 id 生成器补充 id,尽管这种做法不够优雅。示例如下:

const items = ['本站', '张三'];
const genId = (() => {
  let i = 0;
  return () => {
    return i++;
  }
})();
const itemsWithId = items.map(item => ({ id: genId(), val: item }));
// [{id: 0, val: '本站'}, {id: 1, val: '张三'}]

需要注意的是,key 仅需在同一个层级的节点中保持唯一,不要求所有层级的 key 全局唯一。

此外,若确保列表渲染后直至销毁期间不会发生位置变化,可以使用数组索引作为 key。

结尾

对于列表渲染,提供 key 以区分节点至关重要。React 的 DOM Diff 算法将基于 key 调整节点位置,确保涉及内部状态的节点渲染状态正确。

通常情况下,key 值应保证唯一,通常来源于后端返回的数据。在确认列表不会发生位置变更时,可使用数组索引作为 key,以消除控制台警告。

需说明的是,key 并非列表渲染专属,普通节点也可使用 key。

到此这篇关于 React 列表渲染为什么要加 key 的文章就介绍到这了,更多相关 React 列表渲染 key 内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章