Skip to content
This page has been auto-translated and may contain errors.View in English

列表和 key

假设你正在构建一个待办事项列表:几个项目,每个都有自己的文本,来自一个 todo 对象数组。React 没有专门的列表组件来把数组转换成标记。你使用纯 JavaScript 的 map 把每一项转换成一段 JSX,React 就渲染出结果。

jsx
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}

用 map 渲染列表

那个 todos 数组通常来自从拥有实际数据的父组件传下来的 proptodos.map 遍历数组,为每个 todo 返回一个 <li>。外面的花括号把那个 JSX 元素数组直接放进 <ul> 里,就像放一个单独的表达式一样。每个 <li> 还需要一个 key prop,设置为 todo.id。如果你想让列表表现正确,这一部分不是可选的:跳过它,React 仍然会渲染这个列表,但会在控制台记录一个警告,下面描述的重排 bug 一旦列表改变形状就会成为真正的风险。

key 是 React 用来区分列表项的方式,从一次渲染到下一次。它必须在兄弟元素中唯一,所以这个列表中没有两个 <li> 元素共享一个 key,而且它必须是稳定的,意思是相同的 todo 在每次列表重新渲染时都得到相同的 key。来自你的数据的 id,比如 todo.id,正好就是这样:它属于 todo,而不是属于它在数组中的位置,所以即使周围的列表改变,它也保持不变。

为什么数组索引作为 key 不好

这就是为什么数组索引作为 key 不好。这很诱人,因为每个数组已经有一个了:

jsx
{todos.map((todo, index) => (
  <li key={index}>{todo.text}</li>
))}

只要列表从不重排,从不插入或删除项目,这就工作得很好。一旦它这样做,索引就停止匹配它曾经指向的项目。删除第一个 todo,其他每一项都向上移动一个索引,所以 React 看到相同的 key 附加到了不同的 todo。如果那些列表项中有任何一个持有自己的状态,比如一个正在编辑的复选框或有人正在输入的输入框,那个状态会停留在这个索引上,最后出现在错误的行。把索引当作静态列表的备选方案,这些列表从不重排。在其他任何地方都用真正的 id。

好的 key 来自数据,而不是来自渲染。如果你的 todo 来自 API 或数据库,它们几乎肯定已经携带一个 id:直接使用它,而不是推导出新的东西。一个 key 只需要在那一个 map 调用产生的兄弟元素中唯一,不需要在你整个应用中唯一,所以从相同数据构建的 todo 列表和已完成项目列表都可以用 todo.id 作为 key,不会有冲突。React 只在同一时间比较单个子元素集合中的 key。

key 也必须放在 map 直接返回的元素上,而不是嵌套在里面的东西上:

jsx
// 错误:key 在内层元素上,所以 React 看不到它
{todos.map(todo => (
  <li>
    <span key={todo.id}>{todo.text}</span>
  </li>
))}

// 正确:key 在回调返回的最外层元素上
{todos.map(todo => (
  <li key={todo.id}>{todo.text}</li>
))}

React 从数组中每一项的顶层元素读取 key。埋在子元素里面的 key 不算,即使 JSX 中某处存在 key,你仍然会得到"列表中的每个子元素都应该有一个唯一的 key"警告。

索引并非总是错误的。对于一个渲染一次就从不重排、过滤或拼接的列表,比如页脚的一组静态链接,key={index} 就工作得很好,因为索引和它指向的项目从不漂移开。问题开始于那一刻列表可以改变形状:重排行、过滤搜索结果、删除项目。那时候索引开始指向和上次渲染不同的数据,状态和 DOM 节点就会附加到错误的行。

有时数据中根本没有 id,比如一个普通的字符串数组,或者从没有自然标识符的计算构建的数组。在这种情况下,从任何实际上对该列表稳定且唯一的字段组合构建一个复合 key,比如 ${todo.category}-${todo.text},而不是默认使用索引。

Key 是让 React 的协调在列表上正确工作的东西。当一个组件重新渲染时,React 比较新的元素列表和旧的列表,找出需要的最少 DOM 改动集合,它使用 key 在那个比较中匹配元素。两次渲染中相同的 key 意味着 React 把它当作同一个元素:它在原地更新它,保持其 DOM 节点、内部状态和附加到它的其他任何东西。旧渲染中没有匹配的 key 意味着 React 把它当作新的并新鲜挂载它。一个在渲染之间消失的 key 意味着 React 卸载那个元素并丢弃它的状态。

这就是 key 错误时真正破坏的东西。假设一个隐藏在索引 key 后面的列表项持有它自己的 state,比如一个手风琴行上的"展开"标志。重排底层数组而不改变 key,React 仍然会匹配旧的索引 2 到新的索引 2。它看到"同一个元素",重用那个 DOM 节点和它的状态,把展开标志交给现在碰巧坐在索引 2 的任何 todo。没有东西抛出,控制台中没有东西警告你。行静静地显示错误的状态,调试它的人很少一开始就怀疑一个 key 问题。一个稳定的 id 绕过了这个,因为它随数据移动,而不是随数组位置移动,所以协调无论列表如何重排都能匹配正确的元素到正确的项目。

JunoKey 告诉 React 哪个项目是哪个 当你用 map 把数组转成元素列表时,给每个加一个 key,并使用来自你数据的真正 id,而不是它在数组中的位置。key 是 React 如何在渲染之间跟踪哪个元素是哪个的方式,一个属于项目的 id 即使列表被重排也保持正确。
JunoKey 告诉 React 哪个项目是哪个 渲染一个列表就是 array.map 返回 JSX,每一项的最外层元素上有一个 key。使用来自你数据的稳定 id。数组索引看起来像一个快捷方式,但一旦列表可以重排、插入或删除,它就会破坏,因为索引不再和相同的底层项目对齐,状态可能会附加到错误的行。
JunoKey 告诉 React 哪个项目是哪个 Key 是 React 用来在渲染之间协调列表的身份:相同的 key、相同的元素、状态和 DOM 节点被保留;缺失的 key、卸载。索引 key 只对从不改变的列表是安全的。任何重排、插入或删除的东西都需要一个真正的、稳定的 id,否则你会得到状态静静地附加到错误的行,没有错误来指向它。

接下来:State,组件开始在渲染之间记住东西的地方。