陌上人如玉
公子世无双

12.4 为什么需要 key

12.4 为什么需要 key

一句话:key 是 React 用来判断「新旧列表中的元素是否是同一个」的身份标识——它决定组件是「复用」(保留 DOM 与状态)还是「重建」(状态丢失);用 index 会在列表增删排序时造成状态错位。

一、结论先行

问题:为什么列表需要 key?为什么不能用 index?

一句话回答:
  key 是「同层元素的身份标识」——React 用它判断「复用还是重建」。
  用 index 时,index 表示的是「位置」而不是「身份」,
  列表一旦增删/排序,「同一个数据」的 index 就变了,
  于是 React 会把「原本属于 A 的状态」复用给 B → 状态错位。

⭐ 所以:key 必须是「稳定唯一的数据标识」(如数据库 id),
   而不是「位置」(index)或「随机值」。

二、key 的语义

// 没有 key → React 只能用「位置」推断身份(并发出警告)
{items.map((item) => <li>{item.text}</li>)}

// 有 key → React 用 key 判断「是否同一个」
{items.map((item) => <li key={item.id}>{item.text}</li>)}
⭐ React 的判断规则(同层):
① key 相同 + type 相同 → 【复用】(保留 DOM 与组件状态,只更新 props)
② key 相同 + type 不同 → 删除 + 新建
③ key 不同 → 删除 + 新建
④ 新多出来的 → 创建(Placement)
⑤ 旧多出来的 → 删除(ChildDeletion)
操作 key 相同时 key 不同时
更新 props 复用 DOM 重建 DOM
保留内部 state ✅ ❌
保留输入框内容/光标 ✅ ❌(清空并失焦)
保留 CSS 动画进度 ✅ ❌(重新播放)
保留媒体播放位置 ✅ ❌
触发组件卸载/挂载生命周期 ❌ ✅(清理函数会执行)

三、用 index 的 bug 复现

// ❌ 用 index 作 key,列表里每行有一个「输入框」
function BadList() {
  const [items, setItems] = useState(['A', 'B', 'C']);

  return (
    <>
      <button onClick={() => setItems(['NEW', ...items])}>头部插入</button>
      <ul>
        {items.map((text, i) => (
          <li key={i}>
            {text}
            <input placeholder="备注" />
          </li>
        ))}
      </ul>
    </>
  );
}
复现步骤与结果:

1. 初始:[A, B, C] → key = [0, 1, 2]
2. 在 A 那一行的输入框里输入 "hello"
3. 点「头部插入」→ [NEW, A, B, C] → key = [0, 1, 2, 3]

React 的判断(只看 key):
  key 0:之前是 A,现在是 NEW  → 认为「同一个」→ 复用 DOM 与状态
         → 输入框里的 "hello" 保留在「NEW」这一行! ❌
  key 1:之前是 B,现在是 A    → 复用
  key 2:之前是 C,现在是 B    → 复用
  key 3:新增                  → 创建

结果:
· 文本看起来是「对的」(因为文本是「渲染出来的」,会被更新)
· 但【输入框的内容错位了】——原本属于 A 的 "hello" 出现在 NEW 那一行
· 如果行里有「展开状态」「勾选状态」「动画」→ 同样错位
// ✅ 用稳定唯一 ID
function GoodList() {
  const [items, setItems] = useState([
    { id: 'a', text: 'A' },
    { id: 'b', text: 'B' },
    { id: 'c', text: 'C' },
  ]);

  return (
    <>
      <button onClick={() => setItems([{ id: `n${Date.now()}`, text: 'NEW' }, ...items])}>头部插入</button>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            {item.text}
            <input placeholder="备注" />
          </li>
        ))}
      </ul>
    </>
  );
}
// ⭐ 现在 key 跟着「数据」走 → 插入时「同一个数据」始终是同一个组件实例
//    → 输入框内容不会错位
⭐ 三类典型 bug:

① 【头部/中间插入】→ 后续项的 DOM 与状态错位
② 【删除】→ 被删除项的状态「残留」到下一项
   例:[A,B,C](B 的输入框有 "hello")→ 删 B → [A,C],key=[0,1]
   → key 1 从 B 变成 C,复用了「B 的 DOM 与输入内容」→ C 那一行显示 "hello"
③ 【排序】→ 状态跟着「位置」走而不是跟着「数据」走
   → 按价格排序后,「展开的那一行」变成了另一条数据

④ 【有动画的行】→ 动画错位(本应「新增的行」播放入场动画,结果「旧行」播了)

四、为什么 index 是「位置」而不是「身份」

⭐ 根本原因:
  index 是「数据在数组中的下标」——它随「数组的变化」而变。
  而 key 的语义是「这个东西是谁」(身份)——它应该随「数据本身」走。

用 index 作 key 时:
  第 N 项(位置)──(绑定)──> 第 N 个组件实例
  → 当数据移动时,「位置」不变(还是 0,1,2…),但「数据」变了
  → React 以为「是同一个」(位置没变),于是复用了错误的状态

用 id 作 key 时:
  数据 id(身份)──(绑定)──> 组件实例
  → 数据移动时,「身份」跟着数据走 → React 正确复用
⭐ 一个直观的类比:
  key 像是「学生的学号」,index 像是「座位号」。
  按「座位号」认人 → 换了座位就会认错人;
  按「学号」认人 → 换到哪都是同一个人。

五、什么时候可以用 index

// ✅ 可以接受的场景(要写注释说明原因)
// ① 列表「只读、永不重排、永不增删」
{STATIC_OPTIONS.map((opt, i) => <li key={i}>{opt.label}</li>)}

// ② 列表项「完全没有内部状态」(不会有「状态错位」可错)
{tags.map((t, i) => <span key={i}>{t}</span>)}

// ③ 列表是「一次性渲染」且不会更新(如静态文档目录)
{headings.map((h, i) => <a key={i} href={`#${h.id}`}>{h.text}</a>)}
⚠️ 判断标准(三条全满足才可以用 index):
① 列表不会「增、删、排序」(只读)
② 列表项没有「内部状态」(没有输入框、展开态、动画、勾选)
③ 列表项没有「依赖 DOM 状态的东西」(如焦点、选中文本、媒体播放)

⭐ 但即使满足,也建议「有 id 就用 id」——
   因为「今天只读」不代表「明天也不增删」,
   而「用 index」的 bug 往往在「几个月后加了一个功能」时才暴露。

六、没有稳定 ID 时的处理

// ① 最优:让数据「本身带 ID」(入库/接口返回时分配)
// 数据库主键、UUID、雪花 ID 都可以

// ② 次优:在「一次性」生成并保存到数据里
function useItems(raw: string[]) {
  const [items] = useState(() =>
    raw.map((text) => ({ id: crypto.randomUUID(), text }))     // ⭐ 只生成一次
  );
  return items;
}

// ❌ 致命错误:在渲染中生成(每次渲染都是新 id → 整列重建)
{raw.map((text) => <Row key={crypto.randomUUID()} text={text} />)}

// ③ 可用但需谨慎:用「内容 + 类型」拼 key(内容必须唯一且稳定)
key={`${item.type}-${item.title}`}

// ④ 最后手段:index(并写注释说明「列表只读」)
{STATIC_LIST.map((it, i) => <Row key={i} item={it} />)}

七、key 的其它用法(有意为之)

// ✅ ① 用 key 重置组件状态(官方推荐模式)
function ProfilePage({ userId }: { userId: string }) {
  return <Profile key={userId} userId={userId} />;
}
// userId 变化 → 组件重建 → 内部状态全部重置(比「用 Effect 清空每个 state」更可靠)

// ✅ ② 用 key 强制重新播放动画
function FadeIn({ content, animationKey }: { content: string; animationKey: number }) {
  return <div key={animationKey} className="fade-in">{content}</div>;
}

// ✅ ③ 用 key 表达「这两个是不同的东西」
{tab === 'a' ? <Editor key="a" docId="a" /> : <Editor key="b" docId="b" />}
// 同一个 Editor 组件,但代表不同文档 → 用 key 明确区分(避免状态串味)
用途 说明
列表身份 主用途(必须是稳定唯一 ID)
重置组件状态 <Form key={userId} />(切换即重建)
重播动画 改变 key → 元素重建 → CSS 动画重播
强制「重新挂载」 如「重试按钮」用 key 变化来重置子树

八、常见追问

Q: key 会传给子组件吗?
A: 不会。key 是 React 的【保留字段】,不会出现在 props 里。
   子组件需要用它时,要额外传一个 prop:<Row key={id} id={id} />

Q: key 需要「全局唯一」吗?
A: 不需要,只需在【同一层(同一个父元素的子元素列表)内】唯一。
   不同层级、不同列表可以重复用同一个 key。

Q: key 加在「内层元素」上可以吗?
A: 不行(无效)。key 必须加在「map 回调返回的【最外层元素】」上,
   因为 React 是在「比较同层子元素列表」时使用 key。

Q: 用 key 会「重新渲染」吗?
A: 「key 变化」不是「重渲染」,而是「卸载 + 挂载」(代价更大):
   会执行清理函数、丢失状态、重建 DOM。

Q: Fragment 可以带 key 吗?
A: 可以,但【必须用完整写法】:<Fragment key={id}>…</Fragment>
   (简写 <>…</> 不支持 key)。

Q: 列表里「同一层出现重复 key」会怎样?
A: React 会警告,且「复用行为会出错」(后出现的元素会覆盖前面的身份)
   → 表现为「状态错乱」或「渲染结果不对」。

Q: SSR 时 key 有什么注意点?
A: 必须「服务端与客户端生成同样的 key」——
   用「在渲染中生成的随机值」会导致 hydration mismatch(见 9.4)。

Q: key 会影响 diff 的性能吗?
A: 会。key 正确 → 复用(少量 DOM 操作);
   key 错误(如随机值)→ 全量重建(DOM 操作从「几个」变成「几千个」)。

九、常见坑

// ① 用 index 作 key(增删/排序时状态错位)
{items.map((it, i) => <Row key={i} item={it} />)}              // ⚠️

// ② 用随机值 / 时间戳 / 对象作 key(每次渲染整列重建)
{items.map((it) => <Row key={Math.random()} item={it} />)}      // ❌
{items.map((it) => <Row key={Date.now()} item={it} />)}         // ❌
{items.map((it) => <Row key={{ id: it.id }} item={it} />)}      // ❌

// ③ key 加在「内层元素」上
{items.map((it) => <div><Row key={it.id} item={it} /></div>)}   // ❌

// ④ 同一层出现重复 key
{items.map((it) => <Row key="same" item={it} />)}               // ❌

// ⑤ Fragment 简写上写 key(语法错误)
{items.map((it) => <key={it.id}>…</>)}                          // ❌

// ⑥ 在组件「内部」定义组件(每次渲染都是新 type → 相当于「key 一直在变」)
function Parent() {
  function Child() { return <div />; }                          // ❌
  return <Child />;
}

// ⑦ 用 index 作 key 且在列表项里「用 useState」
{items.map((it, i) => <RowWithState key={i} />)}                 // ❌ 最典型的状态错位

// ⑧ 认为「key 只是为了避免警告」
// ⑨ 用「数组索引」以外的「不稳定值」作 key(如「内容」但内容会重复)
// ⑩ 「只有一处使用」的静态列表也强行生成 UUID(没必要)
// ⑪ 用 key 做「性能优化」(不当使用反而更慢)
// ⑫ 忘记「key 要和 type 一起决定复用」(type 不同时 key 相同也没用)

十、面试延伸

  1. 「为什么列表需要 key?」
    key 是「同一层元素的身份标识」——React 用「type + key」共同判断「新旧两个 vnode 是否是同一个节点」:key 相同 + type 相同 → 复用(保留真实 DOM 与组件状态,只更新 props);否则 → 卸载重建(状态丢失)。没有 key 时 React 只能用「位置(index)」推断身份(并发警告),而在列表增删/排序后「位置」不再代表「身份」,于是产生「状态错位」。

  2. 「为什么不能用 index 作 key?」
    因为 index 表示的是「位置」而不是「身份」。列表一旦增删/排序,「同一个数据」的 index 就变了,但 React 只按 key 复用 —— 于是把「原本属于 A 的 DOM 与内部状态」复用给了 B。典型症状:① 列表项里有输入框时,「输入的内容跑到别的行」;② 展开/勾选状态错位;③ 动画错位。只在「列表只读、永不重排、且项内无状态」时才可以接受。

  3. 「用随机值作 key 会怎样?」
    会导致「每次渲染都重建整个列表」——因为每次生成的 key 都不同,React 认为「所有元素都是新的」→ 卸载全部旧节点 + 创建全部新节点。后果是:① 性能灾难(大量 DOM 创建/销毁,尤其长列表);② 状态全部丢失(输入内容、展开态、动画、媒体播放位置);③ 输入框每次都失焦。这是最严重的一类 key 错误(比「用 index」更糟)。

  4. 「key 为什么必须放在 map 返回的最外层元素上?」
    因为 React 是在「比较同一个父元素的子元素列表」时使用 key 的——它需要给「列表中的每一项」一个身份。如果把 key 放在内层,那么「外层的元素」就没有 key(React 只能按「位置」匹配它),而内层的 key 只在这个外层元素的内部有意义,无法表达「这一整项是谁」。所以规范是「key 加在 map 回调返回的最外层元素上」。

  5. 「key 会传给子组件吗?需要全局唯一吗?」
    两个都不会:① key 不会出现在 props 里(它是 React 的保留字段)——子组件需要这个标识时要额外传一个 prop(<Row key={id} id={id} />);② key 不需要全局唯一,只需在「同一层(同一个父元素的子元素列表)内唯一」即可(不同列表、不同层级可以重复)。

  6. 「可以用 key 来重置组件状态吗?」
    可以,而且是官方推荐的模式。当「同一个组件在不同 props 下应该是不同实例」时(如 userId 变化、切换文档),用 <Profile key={userId} /> 就能让 React 在 key 变化时卸载重建,从而自然地重置所有内部状态。这比「在 useEffect 里手动清空每一个 state」更简洁、更不易漏(只需想一次「什么情况下应该重置」),也没有中间态(不像 effect 那样「先渲染出旧状态再清空」)。

  7. 「key 变了会发生什么?」
    会发生「卸载 + 挂载」(而不是「重新渲染」):① 旧组件触发清理函数(useEffect 的 cleanup、DOM 事件解绑);② 旧 DOM 被移除;③ 新组件从头挂载(执行 useState 初始化、useEffect 首次执行);④ 所有状态与 DOM 状态(输入内容、滚动、动画、媒体播放)全部重置。代价比「重渲染」大得多——所以「频繁变化的 key」(如随机值)会造成严重性能问题。

  8. 「key 和『组件内部定义组件』有什么关系?」
    有关系——两者都会导致「type 变化 → 重建」。key 变化是「显式的身份变化」;而「在组件内部定义组件」是「每次渲染都创建新的函数引用」→ React 认为「type 变了」→ 同样卸载重建(状态丢失)。所以两条规范本质相同:① 列表用稳定唯一 key;② 不要在组件内部定义组件(把子组件定义在模块作用域)。它们都是在「保持组件身份稳定」这件事上做对了。

一句话速记

key 是「同层元素的身份标识」——React 用「type + key」判断「复用还是重建」:相同则复用(保留 DOM 与状态),不同则重建(状态丢失);index 是「位置」不是「身份」,列表增删/排序时会让「同一个数据」被复用给错误的组件实例 → 典型症状是「输入框内容跑到别的行」(还有展开态、动画、勾选错位);随机值/时间戳/对象作 key 是灾难(每次渲染整列重建);key 必须加在 map 返回的最外层元素、只在同层唯一、不会出现在 props 里、Fragment 简写不支持 key(要用 <Fragment key>);只在「列表只读 + 项内无状态」时才可以用 index;key 还可以有意用来重置状态/重播动画(<Form key={userId} />);「用 key 重置」比「用 Effect 清空状态」更可靠(无中间态);「组件内定义组件」与「key 变化」都因为「type 变」而导致重建。

赞(0) 打赏
未经允许不得转载:陌上寒 » 12.4 为什么需要 key

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫