在 React 中渲染列表数据是最常见的需求之一。React 使用 JavaScript 原生的数组方法(主要是 map)将数据数组转换为 JSX 数组。为了让 React 在列表变化时高效地更新 DOM,每个列表项都需要一个唯一的 key 属性。
3.5.1 使用 map 渲染列表
map 是 JavaScript 数组的原生方法,用于对数组中的每个元素执行一次回调函数,并返回一个新数组——这正是 React 列表渲染的核心模式:将数据数组映射为 JSX 元素数组。
基础写法:
function FruitList() {
const fruits = ['苹果', '香蕉', '橘子'];
return (
<ul>
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
);
}
数据通常来自 Props、State 或异步请求,但渲染逻辑始终一致:遍历数据,返回 JSX。
渲染对象数组(最常见场景):
function UserList({ users }) {
return (
<div>
{users.map(user => (
<div key={user.id} className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
</div>
);
}
3.5.2 key 属性的核心作用
key 不是给开发者用的,而是 React 内部用于识别列表项的唯一标识。
当列表数据发生变化(增、删、改、排序)时,React 需要对比新旧虚拟 DOM 树。如果没有 key,React 只能按索引顺序逐个比较节点——这可能导致不必要的 DOM 操作、组件状态错乱或性能浪费。
key 的工作机制:
- React 对比新旧列表时,会优先匹配相同
key的节点。 - 如果新旧虚拟 DOM 中某个
key相同,React 认为这是同一个组件实例/元素,仅更新其 Props(如果变化)。 - 如果
key不存在于旧列表中,React 将“创建”该节点;如果旧列表中有key在新列表中消失,React 将“销毁”该节点。
没有 key 或 key 错误时的典型问题:
- 列表顺序变化时产生冗余 DOM 更新
假设列表从 [A, B, C] 变为 [B, C, A]。如果未指定 key,React 会按索引对比:旧[0] 与 新[0] 对比(A→B,内容变化,更新 DOM),旧[1] 与 新[1] 对比(B→C,更新 DOM),旧[2] 与 新[2] 对比(C→A,更新 DOM)。结果三个节点全部被更新,而非仅仅移动位置。
- 组件状态错乱
如果列表项是组件且拥有内部状态(如输入框内容),key 不当会导致状态被错误地保留在错误的位置。例如,用索引作为 key,删除第一项后,原来的第二项变为第一项,但它的输入框状态可能仍保留在 DOM 节点中,因为 React 认为它是同一个元素。
因此,key 保证了列表更新时的一致性和最小化 DOM 操作。
3.5.3 key 的使用规范
✅ 使用稳定、唯一且可预测的标识符
- 首选:数据中的唯一 ID(如数据库主键
item.id)。 - 次选:当没有天然唯一 ID 时,如果列表不会重新排序、过滤或删除,可以使用索引,但需清楚其局限性。
- 绝对禁止:使用随机数(如
Math.random())或每次渲染都变化的 key,这会强制 React 销毁并重新创建所有列表项,导致严重的性能问题和状态丢失。
示例:
{users.map(user => <UserCard key={user.id} user={user} />)}
❌ 避免使用数组索引作为 key(除非列表满足特定条件)
在以下情况下可以使用索引作为 key:
- 列表是静态的,项目不会增删改或重新排序。
- 列表项没有内部状态或非受控 DOM(如表单输入)。
- 列表只是单纯展示数据,不涉及交互状态。
但在大多数真实业务中,列表往往会动态变化,使用索引会引发上述问题。因此,强烈建议始终寻找唯一 ID。
确保 key 在兄弟节点间唯一
key 只需要在同一层级的兄弟节点之间唯一,不需要全局唯一。即不同列表可以拥有相同的 key。
<div>
{fruits.map(f => <li key={f.id}>...</li>)}
</div>
<div>
{vegetables.map(v => <li key={v.id}>...</li>)} // 与上方 key 值可能重复,但互不影响
</div>
key 必须写在 map 的最外层元素上
// ✅ 正确
{items.map(item => <Item key={item.id} item={item} />)}
// ❌ 错误:key 应该放在最外层,而不是内部子元素
{items.map(item => (
<div>
<span key={item.id}>{item.name}</span> // 无效
</div>
))}
如果返回的是 Fragment,可以使用 <React.Fragment key={...}> 或短语法无法加 key,需改用显式 Fragment。
{items.map(item => (
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
3.5.4 列表渲染与性能优化
- 避免在 map 回调中进行复杂计算:如果有计算量大的操作,先预处理数据或使用
useMemo缓存结果。 - 使用
React.memo避免子组件不必要的重渲染:当列表项是组件且数据未发生变化时,配合正确的 key 和 memo 可以显著提升性能。
const MemoizedItem = React.memo(function Item({ item }) {
return <div>{item.name}</div>;
});
// 在父组件中
{items.map(item => <MemoizedItem key={item.id} item={item} />)}
- 虚拟列表:当列表数据量巨大(数千条以上),即使使用正确的 key,大量 DOM 节点仍然会拖慢页面。此时应使用虚拟列表库(如
react-window或react-virtualized),只渲染可视区域的节点,详见第 21 章渲染性能优化。
3.5.5 常见问题与排查
- 控制台警告 “Each child in a list should have a unique ‘key’ prop”
检查是否忘记添加 key,或者 key 被错误地放在了内部元素上。
- 组件状态错乱(比如输入框值串位)
检查 key 是否使用了索引,或者列表中发生了顺序变化但 key 没有反映这种变化。换成稳定 ID 即可解决。
- 列表更新后动画或焦点异常
同样可能是 key 不稳定导致 DOM 节点被复用而非重建。确保 key 唯一且稳定。
掌握 map 遍历与 key 属性的正确用法,是 React 列表渲染基础中的基础。这一看似简单的规则,背后关联着虚拟 DOM 的 Diff 算法和组件状态管理的核心原理,理解它有助于写出高性能、低 Bug 的 React 应用。