R06第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

列表渲染与稳定标识

列表渲染与稳定标识 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

列表渲染人人会写,真正的坑在 key:为什么 React 需要它、下标当 key 什么时候出事、什么才算稳定标识。这一课用一个可重排的任务列表让你亲眼看到错位,读完你能为任何列表选出正确的 key。

问题场景

任务列表带一个需求:每项旁边有个输入框,用来记备注。测试反馈了一个诡异的 bug——把第二项拖到第一位后,原来给「任务 B」写的备注跑到「任务 A」那一行去了:

{tasks.map((task, index) => (
  <TaskRow key={index} task={task} />
))}

数据明明是对的(每项的内容和备注都还在自己的对象里),界面却错位了。问题就出在 key={index}。

核心解释

key 是 React 识别列表项身份的依据。 更新列表时,React 拿新旧两组元素按 key 配对:key 相同的就是「同一个元素」,只更新变化的部分;DOM 节点和组件状态也会跟着 key 走。key 决定了「谁是谁」,而不是「第几个」。

用数组下标当 key,等于告诉 React「第 0 项就是第 0 项」。列表重排后,tasks[0] 的内容从任务 A 变成了任务 B,React 却认为 key=0 的那一行没换人,只是属性变了,于是复用原来的 DOM 节点和输入框——输入框里用户敲的字不属于 React 数据,跟着 DOM 节点留在了原位,内容就错位了。

正确的 key 要求两条:在列表内唯一,且在元素的整个生命周期内稳定。业务数据里现成的唯一标识(id、订单号、slug)就是干这个的:

{tasks.map((task) => (
  <TaskRow key={task.id} task={task} />
))}

排序、插入、删除都按 id 配对:任务 B 挪到第一位,React 知道是「id=B 的元素换了位置」,把整行 DOM 一起搬过去,输入框里的字跟着走,自然不错位。

用下标还有一个更隐蔽的坑:删除中间一项后,后面所有项的下标集体前移,React 会认为「最后一项被删了」,列表项自身的 state(比如展开/折叠标记)会落在错误的元素上。数据没有唯一 id 时,可以在创建时就生成一个(crypto.randomUUID()),而不是用渲染时的下标。

示例拆解

实验区是同一个任务列表的两种实现,关键点如下:

  • 每行包含任务名和一个预填了内容的备注输入框(用 defaultValue 让文字留在 DOM 里,模拟用户已输入);
  • 下标 key 版与 id key 版并排,各自有「插入到最前」「删除第一项」「随机排序」三个按钮,数据源相同、操作同步;
  • 预填内容设计成「第 1 行备注」这种与位置绑定的文字,方便观察错位;
  • 每次操作后先在下标版输入框里改几个字,再做重排操作,对比两边的输入框内容。

验证方式:下标版重排后备注文字停在原位置(DOM 没动);id 版重排后文字跟着任务走(DOM 整行搬移)。

动手任务

打开本课实验区,完成三个操作观察:

  1. 两边都在第一行输入框里输入自己的标记(如「AAA」),点「随机排序」,看「AAA」落在哪一边的任务上。
  2. 点「删除第一项」,观察下标版哪一行的 DOM 被删、id 版哪一行被删。
  3. 把下标版改成 id key(在实验代码里搜 key={index}),确认问题消失——这就是修复本身。

常见错误

  • 用数组下标做会重排列表的 key:插入、删除、排序后输入框内容与组件状态错位;改用稳定业务 id。
  • 用随机数当 key:每次渲染 key 都不同,React 每次都销毁重建全部节点,输入框直接清空、动画失效。
  • 用 Math.random() 或 Date.now() 在渲染时生成 id:同一条数据两次渲染身份不同,等于没有 key;id 要在数据创建时生成并存住。
  • key 取值不唯一:重复 key 触发控制台警告,配对行为不可预测;实在没有唯一字段就用 crypto.randomUUID() 补一个。

检查与复盘

完成下方检查题确认理解。自查清单:

  • 能解释 key 的作用机制:React 按 key 做新旧配对,决定 DOM 与状态复用
  • 知道下标 key 只在「列表永不重排、项无自身状态」时无害,谨慎使用
  • 数据缺 id 时会在创建阶段生成稳定标识,而不是渲染时现造

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1单选简单

渲染列表时给每个元素指定稳定且唯一的 key,主要目的是?

Q2故障定位较难

bug 现场:任务列表支持在顶部插入新任务,每行的「完成」勾选状态存在 TaskItem 自己的 useState 里。用户勾选了「修 bug」和「发版本」,点击「顶部插入任务」后,勾选状态跑到了别的任务上。原因和修法是?

function TaskList() {
  const [tasks, setTasks] = useState([
    { id: 't1', title: '写周报' },
    { id: 't2', title: '修 bug' },
    { id: 't3', title: '发版本' },
  ]);

  function insertAtTop() {
    setTasks([{ id: 't0', title: '紧急插入' }, ...tasks]);
  }

  return (
    <>
      <button onClick={insertAtTop}>顶部插入任务</button>
      {tasks.map((task, index) => (
        <TaskItem key={index} task={task} />
      ))}
    </>
  );
}

function TaskItem({ task }) {
  const [done, setDone] = useState(false);
  return (
    <label>
      <input
        type='checkbox'
        checked={done}
        onChange={(e) => setDone(e.target.checked)}
      />
      {task.title}
    </label>
  );
}

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...