写 JSX 时,你一定见过这个警告:"Each child in a list should have a unique 'key' prop"。
很多人知道要加 key,却不明白为什么 ------ 不就是个标识吗?用 index 当 key 看着也能跑啊? 其实,key 的作用远不止 "消除警告" 这么简单。它是 React Diff 算法的 "导航灯塔",直接影响虚拟 DOM 到真实 DOM 的更新效率。
一、底层原理:React 是如何更新 DOM 的
要理解 key 的作用,必须先搞懂 React 的 "虚拟 DOM" 和 "Diff 算法"------ 这是 React 渲染的核心机制。
1. 虚拟 DOM:内存中的 "DOM 设计图"
真实 DOM 的操作(比如增删改节点)非常昂贵(会触发重排重绘)。React 为了优化性能,在内存中维护了一份和真实 DOM 对应的 "虚拟 DOM"(本质是 JavaScript 对象)。
比如这段 JSX:
jsx
<ul>
<li>标题一</li>
<li>标题二</li>
</ul>
对应的虚拟 DOM 是这样的(简化版):
javascript
{
type: 'ul',
children: [
{ type: 'li', key: null, props: { children: '标题一' } },
{ type: 'li', key: null, props: { children: '标题二' } }
]
}
当组件状态变化时(比如todos
数组更新),React 会生成新的虚拟 DOM,然后对比新旧虚拟 DOM 的差异(这个过程叫 "Diff"),最后只把 "有差异的部分" 更新到真实 DOM,避免全量重绘。
2. Diff 算法:找差异的 "智能工具"
Diff 算法的核心任务是:高效找出新旧虚拟 DOM 的差异,从而最小化真实 DOM 的操作。
对于列表(数组通过 map 生成的节点),Diff 算法的默认逻辑是:按顺序对比子节点 。比如旧列表是[A, B, C]
,新列表是[A, B, D]
,算法会认为前两个节点(A、B)没变,第三个节点从 C 变成了 D,只需更新第三个节点。
但这里有个关键问题:如何判断两个节点是 "同一个节点"? 这就是 key 的作用 ------key 是节点的 "唯一身份证",告诉 React:"带有这个 key 的节点,和上一次的那个是同一个"。
二、key 的本质:节点的 "唯一身份证"
key 的核心作用是:作为虚拟 DOM 节点的唯一标识,帮助 React 在 Diff 过程中准确识别哪些节点是新增、删除或移动的。
没有 key 时,React 会默认用 "节点在列表中的索引" 作为隐式标识。但这在列表顺序变化时会出大问题。
1. 为什么 key 必须唯一?
如果 key 不唯一(比如两个节点用了相同的 key),React 在 Diff 时会混淆这两个节点,导致:
- 错误复用节点状态(比如输入框的值错乱);
- 不必要的节点销毁和重建(性能浪费)。
举个例子:
jsx
// 错误示例:key重复
todos.map((todo, index) => (
<li key={todo.status}> {/* 假设status只有'active'和'completed',会重复 */}
{todo.title}
</li>
))
当两个 todo 的status
相同时,key 重复。React 会认为它们是同一个节点,可能会把第一个节点的状态错误地复用给第二个,导致界面错乱。
2. 为什么 index 作为 key 会踩坑?
用 index 作为 key,在列表顺序不变时(只增删末尾元素)看似没问题,但当列表元素顺序改变(比如在开头插入、删除中间元素、排序)时,会导致大量不必要的重渲染。
我们用一个具体例子分析:
场景:在列表开头插入新元素
当我们执行以下操作时:
jsx
setTodos(prev => [
{
id: 4,
title: '标题四'
},
...prev
]))
我们会发现:每个 <li>
都被重新渲染了 ,而不仅仅是新增的那一个。 当你插入一个新项到数组开头 (如
...prev
的前面),所有项的索引都会改变:
js
// 原数组索引:
0: { id: 1 }
1: { id: 2 }
2: { id: 3 }
// 插入新项到最前面后:
0: { id: 4 } ✅ 新增项
1: { id: 1 } ❌ 原来是 0,现在变成 1
2: { id: 2 } ❌ 原来是 1,现在变成 2
3: { id: 3 } ❌ 原来是 2,现在变成 3
React 看到的是:
- 索引
0
现在是新的<li>
,需要创建。 - 索引
1~3
的<li>
的key
都变了,React 会认为这些是新的元素 ,于是重新创建它们。
这就是为什么 每个 <li>
都被重新渲染了。
当我们为每个 <li>
设置唯一且稳定的 key
jsx
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
使用 todo.id
作为 key
,这样即使你把新项插入到数组开头,React 也能识别哪些项是之前就存在的,只需要创建新的那一项,其他项保持不变。

在
map
中渲染列表时,一定要使用唯一且稳定的key
,不要使用数组索引。
三、真实案例:index 作为 key 导致的 "状态错乱"
除了性能问题,index 作为 key 还可能导致节点状态错乱,尤其是节点包含表单元素时。
jsx
// 问题重现:使用 index 作为 key
function BuggyTodoList() {
const [todos, setTodos] = useState([
{ id: 101, title: '准备早餐' },
{ id: 102, title: '阅读邮件' }
]);
const addTodoAtStart = () => {
const newTodo = { id: Date.now(), title: '紧急任务!' };
setTodos([newTodo, ...todos]); // 在开头添加新任务
};
return (
<div>
<button onClick={addTodoAtStart}>添加紧急任务</button>
<ul>
{todos.map((todo, index) => (
<li key={index}> {/* 问题根源:使用 index 作为 key */}
<input placeholder="输入备注..." />
<span>{todo.title}</span>
</li>
))}
</ul>
</div>
);
}
操作步骤:
- 在第二个任务("阅读邮件")的输入框中输入:"需要回复"
- 点击"添加紧急任务"按钮
问题表现:原本属于"阅读邮件"的输入内容"需要回复",现在出现在了"准备早餐"的输入框中!
根本原因:
- DOM 复用机制:React 通过 key 识别节点是否移动
- index 的不稳定性:当在开头插入元素时,所有后续元素的 index 都变化了
- 状态与 DOM 节点绑定:输入状态绑定在 DOM 节点上,而非数据上
四、最佳实践:什么时候可以用 index 作为 key?
并不是所有场景都不能用 index 作为 key。满足以下条件时,用 index 是安全的:
- 列表是静态的(不会增删、排序);
- 列表项没有状态(如表单输入、组件状态);
- 列表项不会被复用(如简单展示,无交互)。
比如展示一个固定的导航菜单:
jsx
// 可接受场景:静态列表用index作为key
const navItems = ['首页', '关于', '联系'];
navItems.map((item, index) => (
<li key={index}>{item}</li>
))
总结:key 的核心作用与底层逻辑
key 的本质是虚拟 DOM 节点的唯一标识,它的核心作用是帮助 React 的 Diff 算法:
- 准确识别节点的新增、删除和移动;
- 避免不必要的节点重渲染和状态错乱;
- 最小化真实 DOM 操作,提升性能。
记住三个原则:
- 唯一:key 在兄弟节点中必须唯一;
- 稳定:key 在节点生命周期中不能改变(如用数据 id,不用 index);
- 简洁:key 尽量用简单值(如数字、字符串),避免复杂对象。
下次写 map 循环时,别再随便用 index 当 key 了 ------ 看似省了点事,实则可能埋下性能和状态错乱的坑。用数据的唯一 id,才是既规范又高效的做法。