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 数组通常作为 prop 从拥有数据的父组件传下来。todos.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 后面并持有自己的状态,比如手风琴行上的"展开"标志。在不改 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,组件开始在渲染间记住东西的地方。