跳至正文
来两杯美式
返回

React 核心(11):Key 与列表渲染

By 来两杯美式
发布于

key 是 React 识别列表元素身份的标签——有了它,React 才知道”哪个是哪个”,而不是”第几个是第几个”。


一、为什么列表需要 key

1.1 没有 key 会怎样

React 更新 UI 的方式是对比新旧虚拟 DOM,做最小化修改。这个对比过程叫 Diff,核心规则见 声明式与React的实现机制

对于列表,Diff 遇到一个问题:怎么知道新列表里的某个元素,对应旧列表里的哪个?

// 旧渲染结果
<li>Alice</li>
<li>Bob</li>
<li>Charlie</li>

// 新渲染结果(头部插入了 Dave)
<li>Dave</li>
<li>Alice</li>
<li>Bob</li>
<li>Charlie</li>

没有 key,React 只能按位置匹配:

位置React 的理解
第 1 个AliceDave“内容从 Alice 变成了 Dave”
第 2 个BobAlice“内容从 Bob 变成了 Alice”
第 3 个CharlieBob“内容从 Charlie 变成了 Bob”
第 4 个Charlie“新增了第 4 个元素”

结果:4 次更新。而实际只需在头部插入 1 个元素。

1.2 有了 key 之后

// 旧
<li key="a">Alice</li>
<li key="b">Bob</li>
<li key="c">Charlie</li>

// 新
<li key="d">Dave</li>
<li key="a">Alice</li>
<li key="b">Bob</li>
<li key="c">Charlie</li>

React 按 key 匹配:key=“a” 还是 Alice,key=“b” 还是 Bob——都没变,不用更新。只有 key=“d” 是新的,插入即可。1 次插入,0 次更新。

💡 生活类比:key 像座位号——有座位号,换座位能找到人;没有座位号,只能按”第几个座位”找,人一换就找错。

1.3 不加 key 的警告

列表没给 key 时,React 控制台会报警:

Warning: Each child in a list should have a unique "key" prop.

这不是建议,是必须。没有 key 时 React 退回到按位置匹配(等价于 key={index}),性能和正确性都可能出问题。


二、key 的本质

2.1 兄弟节点间的唯一标识

key 是兄弟节点间的唯一标识,帮 React 在 Diff 时做最小化更新。“兄弟节点”指同一个父节点下的同一层子节点:

<ul>
  <li key="a">Alice</li> {/* key="a" 只需在 <ul> 的直接子 <li> 间唯一 */}
  <li key="b">Bob</li> {/* 不需要跟其他 <ul> 下的 key 不同 */}
</ul>

2.2 key 不需要全局唯一

function Page() {
  return (
    <div>
      <ul>
        <li key="a">水果:苹果</li>
        <li key="b">水果:香蕉</li>
      </ul>
      <ol>
        <li key="a">蔬菜:白菜</li> {/* ✅ 跟上面的 "a" 不冲突 */}
        <li key="b">蔬菜:萝卜</li>
      </ol>
    </div>
  );
}

💡 key 的唯一性范围 = 同一父节点的直接子节点之间。不同父节点下的 key 互不干扰。

2.3 key 是给 React 用的,不是给你的

key 不会作为 prop 传进组件:

function Item({ id, name }) {
  // 拿不到 key——key 被 React 消费了,不会出现在 props 里
}

<li key="abc">
  <Item id={1} name="Alice" />
</li>;

需要组件内部用到那个值?显式传一个 prop

<li key={item.id}>
  <Item id={item.id} name={item.name} />
</li>
//              ↑ React 用          ↑ 组件内部用

三、用 index 作 key 的问题

最常见的坑:

{
  items.map((item, index) => <li key={index}>{item.name}</li>);
}

看起来没问题,跑起来也没报错。但列表一旦增删或重排,就会出 bug。

3.1 为什么 index 作 key 会错位

index 是位置编号,不是身份标识。列表变化后,同一个 index 指向的元素变了:

初始:index 0=Alice, 1=Bob, 2=Charlie
头部插入 Dave 后:index 0=Dave, 1=Alice, 2=Bob, 3=Charlie

React 按 key(即 index)匹配:key=0 从 Alice 变成 Dave → React 以为”Alice 的内容变了”,更新而不是移动。又变成了 4 次操作,跟没有 key 一样。

3.2 具体示例:状态错位 bug

index 作 key 最致命的问题不是性能,而是状态错位

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: "学习 React" },
    { id: 2, text: "写代码" },
    { id: 3, text: "睡觉" },
  ]);

  return (
    <div>
      {/* ❌ 用 index 作 key */}
      {todos.map((todo, index) => (
        <TodoItem key={index} todo={todo} />
      ))}
      <button
        onClick={() => {
          setTodos([{ id: 4, text: "新任务" }, ...todos]);
        }}
      >
        添加到头部
      </button>
    </div>
  );
}

function TodoItem({ todo }) {
  const [editing, setEditing] = useState(false);
  // 用户正在编辑 index=1("写代码")
  // 插入新项后 index=1 变成了"学习 React"
  // 但 React 以为 key=1 还是同一个组件实例
  // → editing 状态留在了 index=1,输入框突然变成另一项!
}
// ✅ 用 id 作 key,问题消失
{
  todos.map(todo => <TodoItem key={todo.id} todo={todo} />);
}
// key 跟着数据走,不管位置怎么变,React 都能正确追踪

3.3 什么时候用 index 没问题

纯静态展示、不会增删重排的列表:

// ✅ 不会变的常量列表,用 index 没问题
const WEEKDAYS = ["周一", "周二", "周三", "周四", "周五"];
{
  WEEKDAYS.map((day, index) => <li key={index}>{day}</li>);
}

条件:列表不会增删不会重排、列表项没有内部状态。三条全满足才安全。

💡 简单判断:列表可能变(增、删、排序、过滤),就不要用 index。


四、key 的正确选择

4.1 数据的唯一 ID

// ✅ 后端返回的数据通常有 id
{
  users.map(user => <UserCard key={user.id} user={user} />);
}

// ✅ 本地创建的数据,用 uuid
const addTodo = text => {
  setTodos([...todos, { id: crypto.randomUUID(), text }]);
};

4.2 组合 key

单一字段不唯一时,用多个字段组合:

// ✅ 同一用户可能在多个组,单用 userId 不唯一
{
  groups.map(group =>
    group.members.map(member => (
      <MemberCard key={`${group.id}_${member.userId}`} member={member} />
    ))
  );
}

4.3 选择 key 的原则

原则说明
稳定同一条数据,key 永远一样
唯一同一兄弟组内,key 不重复
可预测不要用随机值(每次渲染都变)
有意义key 跟数据身份绑定,跟位置无关

五、列表渲染的完整模式

5.1 Array.map() 返回 JSX 数组

function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          <span>{user.name}</span>
          <span>{user.email}</span>
        </li>
      ))}
    </ul>
  );
}

为什么是 map 而不是 forEach? map 返回新数组,forEach 不返回值。JSX 里需要表达式,map 的返回值正好是 JSX 数组。

5.2 Fragment 作为列表容器

列表项不需要额外 DOM 包裹时,用 Fragment:

function DescriptionList({ items }) {
  return (
    <dl>
      {items.map(item => (
        // ✅ key 放在 Fragment 上
        <React.Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </React.Fragment>
      ))}
    </dl>
  );
}
// ❌ 短语法 Fragment 不能传 key
<></>

// ✅ 需要传 key 时,必须用完整写法
<React.Fragment key={item.id}></React.Fragment>

5.3 列表为空的处理

function UserList({ users }) {
  if (users.length === 0) {
    return <p className="empty-hint">暂无用户数据</p>;
  }
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

六、key 与组件实例的关系

6.1 key 变了 = 销毁旧实例 + 创建新实例

key 变了,React 会销毁旧组件,创建新组件。 这是最容易被忽视的特性:

function Editor({ userId }) {
  const [content, setContent] = useState("");
  // ❌ userId 变了,但组件实例没变,内部状态会残留
  return (
    <textarea value={content} onChange={e => setContent(e.target.value)} />
  );
}

// ✅ key 变了 → 组件重新挂载 → 状态全部重置
<Editor key={userId} userId={userId} />;

6.2 利用 key 强制重新挂载

表单重置——提交后清空所有输入:

function ContactForm() {
  const [formKey, setFormKey] = useState(0);
  const handleSubmit = () => {
    submitForm();
    setFormKey(k => k + 1); // key 变了 → 表单组件重新挂载 → 输入清空
  };
  return (
    <div>
      <Form key={formKey} />
      <button onClick={handleSubmit}>提交</button>
    </div>
  );
}

切换标签页时重置状态

// key 变了 → 旧面板销毁,新面板创建,状态不残留
<TabPanel key={activeTab} tabId={activeTab} />

💡 记住:key 变 → 销毁重建;key 不变 → 复用更新。这个规则是 key 一切行为的根基。


七、常见陷阱

7.1 用 Math.random() 作 key

// ❌ 每次渲染 key 都不一样
{
  items.map(item => <li key={Math.random()}>{item.name}</li>);
}

比没有 key 还糟糕——每次渲染所有 key 都变,React 销毁并重建所有列表项。状态丢失,DOM 操作暴增。

💡 key 的核心价值是稳定Math.random() 完全违背了这个原则。

7.2 用 index + 动态列表

// ❌ 列表会增删,却用 index 作 key
{
  todos.map((todo, index) => <TodoItem key={index} todo={todo} />);
}

增删时状态错位,详见第三章。修复:给每条数据加 id。

7.3 key 相同但内容不同

// ❌ 所有项用了同一个 key
{
  items.map(item => <li key="item">{item.name}</li>);
}

// ❌ 更隐蔽:数据源 id 重复
const data = [
  { id: 1, name: "Alice" },
  { id: 1, name: "Alice (duplicate)" }, // id 重复!
];
{
  data.map(item => (
    <li key={item.id}>{item.name}</li> // 两个 key=1
  ));
}

React 会警告 Encountered two children with the same key修复:确保 key 唯一,数据有重复时用组合 key:

// ✅ 加 index 区分重复 id
{
  data.map((item, index) => <li key={`${item.id}_${index}`}>{item.name}</li>);
}

7.4 陷阱速查表

陷阱后果修复
key={Math.random()}每次渲染全部重建用数据的稳定 id
key={index} + 动态列表状态错位用数据的稳定 id
所有项 key 相同只渲染最后一项确保 key 唯一
数据源 id 重复警告 + 渲染异常用组合 key

八、一句话总结

key 是列表元素的身份证号——稳定、唯一、跟数据走。用 id 不用 index,key 变了就重建,不变就复用。


相关


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
React
第 11 / 17 篇
查看系列全部文章
  1. 01.React 核心(1):React 组件与函数的本质区别
  2. 02.React 核心(2):声明式与 React 的实现机制
  3. 03.React 核心(3):Props 完全指南
  4. 04.React 核心(4):useState 完全指南
  5. 05.React 核心(5):useEffect 完全指南
  6. 06.React 核心(6):useRef 完全指南
  7. 07.React 核心(7):Context API 与状态共享
  8. 08.React 核心(8):useCallback 与 useMemo
  9. 09.React 核心(9):Hooks 规则与闭包陷阱
  10. 10.React 核心(10):自定义 Hook 设计
  11. 11.React 核心(11):Key 与列表渲染
  12. 12.React 核心(12):表单与受控/非受控组件
  13. 13.React 核心(13):React 渲染机制深入——Fiber 与调度
  14. 14.React 核心(14):React 18 并发特性
  15. 15.React 核心(15):Suspense 机制详解
  16. 16.React 核心(16):TanStack Query 入门指南
  17. 17.React 核心(17):TanStack Query 缓存机制详解

上一篇
React 核心(12):表单与受控/非受控组件
下一篇
React 核心(10):自定义 Hook 设计