Skip to content

React 中的 key 属性有什么作用?

##深入理解 React 中的 key:它为何如此重要?

React 开发中,当我们渲染一个列表时,几乎总会看到控制台提示一个熟悉的警告:Each child in a list should have a unique "key" prop。

许多初学者可能会通过 key={index} 快速“解决”这个警告,但这样做有时会埋下隐患。key 属性的意义远不止是消除一个警告,它与 React 的核心工作机制——协调(Reconciliation)算法紧密相关。

理解 key 的真正作用,是深入掌握 React 性能优化和组件状态管理的关键一步。

key 的核心使命:元素的 身份证

我们可以将 key 想象成列表中每个元素的唯一身份证

React 渲染一个列表时,它会生成一个虚拟 DOM 树。当列表数据发生变化(例如,增、删、改、重排),React 需要高效地更新真实 DOM。它通过对比新旧两棵虚拟 DOM 树的差异,找出最小的变更集来执行更新,这个过程就是 协调

如果没有 keyReact 只能通过元素在列表中的**位置(index)**来逐个对比。我们来看一个场景:

假设我们有一个简单的列表:

javascript
<ul>
  <li>苹果</li>
  <li>香蕉</li>
</ul>

现在,我们在列表的开头插入一个新项目“橙子”:

javascript
<ul>
  <li>橙子</li>
  <li>苹果</li>
  <li>香蕉</li>
</ul>

React 的视角里,如果没有 key,它会这样对比:

    1. “咦,第一个 <li> 的内容从‘苹果’变成了‘橙子’,我需要更新它。”
    1. “第二个 <li> 的内容从‘香蕉’变成了‘苹果’,我也需要更新它。”
    1. “哦,最后多出来一个 <li> 叫‘香蕉’,我需要新建并插入它。”

这显然不是最高效的方式。实际上,我们只是插入了一个元素,而原有的两个元素并没有改变。这种基于索引的对比,导致了大量不必要的 DOM 操作。

现在,我们为列表加上 key

javascript
// 初始状态
<ul>
  <li key="apple">苹果</li>
  <li key="banana">香蕉</li>
</ul>

// 更新后
<ul>
  <li key="orange">橙子</li>
  <li key="apple">苹果</li>
  <li key="banana">香蕉</li>
</ul>

有了 key 这个身份证React 的对比过程就变得智能多了:

    1. React 看到 key="orange" 是一个全新的元素,于是新建它。
    1. React 在旧的列表中找到了 key="apple"key="banana",发现它们只是位置移动了,内容没有变化。于是,React 不会重新创建它们,而是高效地移动现有的 DOM 节点到新的位置。

通过 keyReact 能够准确识别出哪些元素是新增的、哪些是删除的、哪些只是移动了位置,从而最大限度地重用已有的 DOM 结构,大大提升了性能。

key 的三个基本原则

一个合格的 key 需要满足以下三个条件:

    1. 唯一性(Unique)key 在兄弟节点之间必须是唯一的。一个列表中不能出现两个相同的 key
    1. 稳定性(Stable)key 的值必须是稳定且可预测的。同一个元素在多次渲染中应该保持相同的 key。我们不应该在渲染过程中使用 Math.random() 或其他不稳定的值来生成 key
    1. 可关联性(Associated)key 应该与列表中的数据项本身相关联。通常,我们使用数据项中的唯一标识符,如 item.id

为什么不推荐使用 index 作为 key

使用数组索引 key={index} 是一个常见的 反模式,尤其是在列表会发生变化的情况下。我们来看一个包含输入框的列表:

javascript
function ToDoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React' },
    { id: 2, text: '写一篇博客' },
  ]);

  // ... 删除第一个 todo 的逻辑
  
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index}>
          <input type="text" defaultValue={todo.text} />
        </li>
      ))}
    </ul>
  );
}

假设我们在这个列表中,删除了第一项 学习 React

  • 期望行为:第一个 <li> 连同其内部的输入框一起被移除。
  • 实际行为(当 key={index} 时)**:
    1. 删除前,列表是:
    • <li key={0}> (输入框值为 "学习 React")
    • <li key={1}> (输入框值为 "写一篇博客")
    1. 删除数据后,todos 数组变为 [{ id: 2, text: '写一篇博客' }]React 重新渲染。
    2. React 对比新旧列表:
    • 新的 <li key={0}> (对应 "写一篇博客") 与旧的 <li key={0}> 对比。React 认为这是同一个组件,因此复用了旧的 DOM 节点,仅仅是将 defaultValue 更新为 "写一篇博客"。
    • 旧的 <li key={1}> 被认为是多余的,因此被删除。

结果就是,我们看到的界面上,第一个输入框的内容没有变(如果它是非受控组件),或者状态出现混乱。这是因为 React 错误地复用了组件实例和它关联的内部状态。

如果使用 key={todo.id}React 就能精确地知道 key={1} 的元素被删除了,而 key={2} 的元素依然存在,从而正确地执行 DOM 操作。

那么,什么时候可以使用 index 作为 key

虽然不推荐,但在极少数情况下,使用 index 作为 key 是可以接受的,前提是列表同时满足以下所有条件:

    1. 列表和项目是静态的,不会进行计算,也不会改变。
    1. 列表中的项目没有稳定唯一的 ID
    1. 列表永远不会被重新排序或过滤

在这种严格的条件下,使用 index 不会引入状态管理问题。但在绝大多数动态场景中,我们都应该优先使用数据自身的唯一标识作为 key

key 的另一个妙用:强制组件重新挂载

除了在列表中使用,key 还有一个强大的用途:key 的值发生变化时,React 会销毁旧的组件实例并创建一个全新的实例

这意味着组件的 constructor 会重新执行,所有内部状态(state)都会被重置到初始值。

这在某些场景下非常有用。例如,我们有一个用户资料页面组件 UserProfile,它通过 props 接收 userId 来获取并展示用户信息。

javascript
<UserProfile userId={currentUser.id} />

当用户从一个人的资料页切换到另一个人的资料页时,currentUser.id 会改变。如果我们希望每次切换用户时,UserProfile 组件都完全重置(例如,清空旧数据、重置加载状态、重新触发 useEffect),而不是在现有组件上更新,我们可以这样做:

javascript
<UserProfile key={currentUser.id} userId={currentUser.id} />

通过将 userId 同时用作 key,我们向 React 传达了一个明确的信号:当 key(也就是 userId)改变时,这是一个完全不同的 UserProfile 实例,请丢弃旧的,创建一个新的。这是一种非常简洁而声明式地管理组件生命周期和状态的方式。

总结

keyReact 中扮演着至关重要的角色,它不仅仅是为了消除警告。

  • 核心作用:keyReact 在协调过程中识别元素的唯一标识,帮助 React 高效地增、删、改和重排元素
  • 最佳实践:始终使用从数据中获取的稳定且唯一的标识(如 item.id)作为 key
  • 避免使用 index**:在动态列表(可排序、过滤、增删)中,使用 index 作为 key 会导致性能问题和难以察觉的状态 bug
  • 强制重载:改变一个组件的 key 可以强制该组件重新挂载,这是一种重置其内部状态的有效策略。

深入理解并正确使用 key,能帮助我们构建出更健壮、性能更优的 React 应用。从今天起,认真对待每一个 key 吧

不知道说啥了很无语了