人人都会AI编程

3.5 计算属性与侦听器基础用法与区别

更新时间:2026-07-10

在 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 错误时的典型问题:

  1. 列表顺序变化时产生冗余 DOM 更新

假设列表从 [A, B, C] 变为 [B, C, A]。如果未指定 key,React 会按索引对比:旧[0]新[0] 对比(A→B,内容变化,更新 DOM),旧[1]新[1] 对比(B→C,更新 DOM),旧[2]新[2] 对比(C→A,更新 DOM)。结果三个节点全部被更新,而非仅仅移动位置。

  1. 组件状态错乱

如果列表项是组件且拥有内部状态(如输入框内容),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-windowreact-virtualized),只渲染可视区域的节点,详见第 21 章渲染性能优化。

3.5.5 常见问题与排查

  1. 控制台警告 “Each child in a list should have a unique ‘key’ prop”

检查是否忘记添加 key,或者 key 被错误地放在了内部元素上。

  1. 组件状态错乱(比如输入框值串位)

检查 key 是否使用了索引,或者列表中发生了顺序变化但 key 没有反映这种变化。换成稳定 ID 即可解决。

  1. 列表更新后动画或焦点异常

同样可能是 key 不稳定导致 DOM 节点被复用而非重建。确保 key 唯一且稳定。

掌握 map 遍历与 key 属性的正确用法,是 React 列表渲染基础中的基础。这一看似简单的规则,背后关联着虚拟 DOM 的 Diff 算法和组件状态管理的核心原理,理解它有助于写出高性能、低 Bug 的 React 应用。