7.React 中的 ⁠key 属性有什么作用? 副本

查看飞书原文 ↗3,811 字符
深入理解 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 树的差异,找出最小的变更集来执行更新,这个过程就是“协调”。​
​
如果没有 key,React 只能通过元素在列表中的**位置(index)**来逐个对比。我们来看一个场景:​
​
假设我们有一个简单的列表:​
​
<ul>
  <li>苹果</li>
  <li>香蕉</li>
</ul>
​
现在,我们在列表的开头插入一个新项目“橙子”:​
​
<ul>
  <li>橙子</li>
  <li>苹果</li>
  <li>香蕉</li>
</ul>
​
在 React 的视角里,如果没有 key,它会这样对比:​
​
1.
“咦,第一个 <li> 的内容从‘苹果’变成了‘橙子’,我需要更新它。”​
2.
“第二个 <li> 的内容从‘香蕉’变成了‘苹果’,我也需要更新它。”​
3.
“哦,最后多出来一个 <li> 叫‘香蕉’,我需要新建并插入它。”​
​
这显然不是最高效的方式。实际上,我们只是插入了一个元素,而原有的两个元素并没有改变。这种基于索引的对比,导致了大量不必要的 DOM 操作。​
​
现在,我们为列表加上 key:​
​
// 初始状态
<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" 是一个全新的元素,于是新建它。​
2.
React 在旧的列表中找到了 key="apple" 和 key="banana",发现它们只是位置移动了,内容没有变化。于是,React 不会重新创建它们,而是**高效地移动**现有的 DOM 节点到新的位置。​
​
通过 key,React 能够准确识别出哪些元素是新增的、哪些是删除的、哪些只是移动了位置,从而最大限度地重用已有的 DOM 结构,大大提升了性能。​
​
key 的三个基本原则​
​
一个合格的 key 需要满足以下三个条件:​
​
1.
唯一性(Unique):key 在兄弟节点之间必须是唯一的。一个列表中不能出现两个相同的 key。​
2.
稳定性(Stable):key 的值必须是稳定且可预测的。同一个元素在多次渲染中应该保持相同的 key。我们不应该在渲染过程中使用 Math.random() 或其他不稳定的值来生成 key。​
3.
可关联性(Associated):key 应该与列表中的数据项本身相关联。通常,我们使用数据项中的唯一标识符,如 item.id。​
​
为什么不推荐使用 index 作为 key?​
​
使用数组索引 key={index} 是一个常见的“反模式”,尤其是在列表会发生变化的情况下。我们来看一个包含输入框的列表:​
​
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} 时)**:​
a.
删除前,列表是:​
▪
<li key={0}> (输入框值为 "学习 React")​
▪
<li key={1}> (输入框值为 "写一篇博客")​
b.
删除数据后,todos 数组变为 [{ id: 2, text: '写一篇博客' }]。React 重新渲染。​
c.
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.
列表和项目是**静态**的,不会进行计算,也不会改变。​
2.
列表中的项目**没有稳定唯一的 ID**。​
3.
列表**永远不会被重新排序或过滤**。​
​
在这种严格的条件下,使用 index 不会引入状态管理问题。但在绝大多数动态场景中,我们都应该优先使用数据自身的唯一标识作为 key。​
​
key 的另一个妙用:强制组件重新挂载​
​
除了在列表中使用,key 还有一个强大的用途:**当 key 的值发生变化时,React 会销毁旧的组件实例并创建一个全新的实例**。​
​
这意味着组件的 constructor 会重新执行,所有内部状态(state)都会被重置到初始值。​
​
这在某些场景下非常有用。例如,我们有一个用户资料页面组件 UserProfile,它通过 props 接收 userId 来获取并展示用户信息。​
​
<UserProfile userId={currentUser.id} />
​
当用户从一个人的资料页切换到另一个人的资料页时,currentUser.id 会改变。如果我们希望每次切换用户时,UserProfile 组件都完全重置(例如,清空旧数据、重置加载状态、重新触发 useEffect),而不是在现有组件上更新,我们可以这样做:​
​
<UserProfile key={currentUser.id} userId={currentUser.id} />
​
通过将 userId 同时用作 key,我们向 React 传达了一个明确的信号:当 key(也就是 userId)改变时,这是一个完全不同的 UserProfile 实例,请丢弃旧的,创建一个新的。这是一种非常简洁而声明式地管理组件生命周期和状态的方式。​
​
总结​
​
key 在 React 中扮演着至关重要的角色,它不仅仅是为了消除警告。​
​
•
核心作用:key 是 React 在协调过程中识别元素的唯一标识,帮助 React 高效地增、删、改和重排元素。​
•
最佳实践:始终使用从数据中获取的**稳定且唯一**的标识(如 item.id)作为 key。​
•
避免使用 index**:在动态列表(可排序、过滤、增删)中,使用 index 作为 key 会导致性能问题和难以察觉的状态 bug。​
•
强制重载:改变一个组件的 key 可以强制该组件重新挂载,这是一种重置其内部状态的有效策略。​
​
深入理解并正确使用 key,能帮助我们构建出更健壮、性能更优的 React 应用。从今天起,认真对待每一个 key 吧。​
​