
一句话:
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 相同也没用)
十、面试延伸
- 「为什么列表需要
key?」
key是「同一层元素的身份标识」——React 用「type+key」共同判断「新旧两个 vnode 是否是同一个节点」:key相同 +type相同 → 复用(保留真实 DOM 与组件状态,只更新 props);否则 → 卸载重建(状态丢失)。没有key时 React 只能用「位置(index)」推断身份(并发警告),而在列表增删/排序后「位置」不再代表「身份」,于是产生「状态错位」。 -
「为什么不能用
index作key?」
因为index表示的是「位置」而不是「身份」。列表一旦增删/排序,「同一个数据」的 index 就变了,但 React 只按key复用 —— 于是把「原本属于 A 的 DOM 与内部状态」复用给了 B。典型症状:① 列表项里有输入框时,「输入的内容跑到别的行」;② 展开/勾选状态错位;③ 动画错位。只在「列表只读、永不重排、且项内无状态」时才可以接受。 -
「用随机值作
key会怎样?」
会导致「每次渲染都重建整个列表」——因为每次生成的key都不同,React 认为「所有元素都是新的」→ 卸载全部旧节点 + 创建全部新节点。后果是:① 性能灾难(大量 DOM 创建/销毁,尤其长列表);② 状态全部丢失(输入内容、展开态、动画、媒体播放位置);③ 输入框每次都失焦。这是最严重的一类key错误(比「用 index」更糟)。 -
「
key为什么必须放在map返回的最外层元素上?」
因为 React 是在「比较同一个父元素的子元素列表」时使用key的——它需要给「列表中的每一项」一个身份。如果把key放在内层,那么「外层的元素」就没有key(React 只能按「位置」匹配它),而内层的key只在这个外层元素的内部有意义,无法表达「这一整项是谁」。所以规范是「key加在map回调返回的最外层元素上」。 -
「
key会传给子组件吗?需要全局唯一吗?」
两个都不会:①key不会出现在 props 里(它是 React 的保留字段)——子组件需要这个标识时要额外传一个 prop(<Row key={id} id={id} />);②key不需要全局唯一,只需在「同一层(同一个父元素的子元素列表)内唯一」即可(不同列表、不同层级可以重复)。 -
「可以用
key来重置组件状态吗?」
可以,而且是官方推荐的模式。当「同一个组件在不同 props 下应该是不同实例」时(如userId变化、切换文档),用<Profile key={userId} />就能让 React 在key变化时卸载重建,从而自然地重置所有内部状态。这比「在useEffect里手动清空每一个 state」更简洁、更不易漏(只需想一次「什么情况下应该重置」),也没有中间态(不像 effect 那样「先渲染出旧状态再清空」)。 -
「
key变了会发生什么?」
会发生「卸载 + 挂载」(而不是「重新渲染」):① 旧组件触发清理函数(useEffect的 cleanup、DOM 事件解绑);② 旧 DOM 被移除;③ 新组件从头挂载(执行useState初始化、useEffect首次执行);④ 所有状态与 DOM 状态(输入内容、滚动、动画、媒体播放)全部重置。代价比「重渲染」大得多——所以「频繁变化的 key」(如随机值)会造成严重性能问题。 -
「
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 变」而导致重建。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!