一、从一个「界面错乱」的案例说起
你是否遇到过这样的场景:当给 todos 列表头部插入新任务时,明明只新增了一项,后面的所有任务却突然「集体漂移」,甚至输入框里的内容不翼而飞?这大概率是因为你给 map
循环写了一个「假 key」------ 用数组索引(index)当 key 啦!
javascript
// 错误示范:用 index 当 key
{todos.map((todo, index) => (
<li key={index}>{todo.title}</li>
))}
别小看这个看似不起眼的 key,它可是 React diff 算法的「眼睛」。接下来我们就从原理到实战,彻底搞懂这个高频面试考点。
二、key 的核心使命:让 React 看懂「谁变了」
(一)React 渲染的「高效魔法」:diff 算法
当状态(如 todos)变化时,React 不会傻乎乎地重新渲染整个列表,而是通过 diff 算法 对比新旧虚拟 DOM,只更新真正变化的部分。这时候,key 就像每个列表项的「身份证」,告诉 React:
「这个列表项对应的是旧数据里的谁?它的位置 / 内容是否真的变了?」
(二)key 的两大硬核作用
- 精准定位更新:避免不必要的 DOM 操作(比如重排重绘),提升性能。
- 保持状态稳定:确保表单输入、组件生命周期等状态与数据正确绑定,不会因为列表重排而「张冠李戴」。
三、用 index 当 key?这三个坑让你原地翻车!
(一)坑一:数组增删引发「状态错乱」
想象一个待办事项列表,当你在头部插入新任务时:
javascript
// 向数组头部插入新任务
setTodos(pre => [{ id: 4, title: "标题四" }, ...pre]);
如果用 index
当 key,原本索引为 0 的「标题一」会变成索引 1,React 会误以为「标题一」对应的 DOM 节点应该复用索引 1 的位置,导致:
-
界面显示顺序错乱(旧任务的位置对不上新数据)。
-
输入框、勾选状态等局部状态丢失(比如用户刚编辑完的任务,刷新后内容跑到别的项去了)。
这就像把学生的座位号按排队顺序动态分配,一旦有人插在队首,后面所有人的座位号都变了,老师根本分不清谁是谁!
(二)坑二:性能暴跌!无效重渲染满天飞
React 默认用 key 对比新旧节点,当 index 变化时,即使列表项内容没改,React 也会认为「这个节点是全新的」,触发不必要的重渲染。比如案例中只修改第一个任务的标题,却导致整个列表重新渲染,白白浪费性能。
(三)坑三:diff 算法「智商掉线」的本质原因
index 作为 key 时,它的值依赖数组顺序,而数组顺序是「动态变化的」。当数据发生以下变化时:
- 插入 / 删除非末尾元素(如头部插入新任务)。
- 排序(如拖拽调整任务顺序)。
index 会批量改变,导致 React 的 diff 算法无法正确匹配新旧节点,只能从头开始暴力对比,diff 效率从 O (n) 退化为 O (n²)。
四、正确姿势:key 必须满足「唯一性」和「稳定性」
(一)最佳实践:用数据自带的唯一标识(如 id)
如果数据有像数据库主键这样的唯一标识(如案例中的 id
),直接用它!
javascript
// 正确示范:用数据自带的 id 当 key
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
这样无论数组顺序怎么变,每个列表项的 key 都不变,React 能精准识别「谁是旧节点,谁是新节点」。
(二)如果数据没有 id?自己造一个!
如果是临时生成的数据(比如表单录入的列表),可以在数据生成时添加唯一标识:
javascript
// 生成唯一 ID(时间戳+随机数,简单够用)
const [todos, setTodos] = useState([
{ id: 1, title: "标题一" },
{ id: 2, title: "标题二" },
// 新增时生成 id
]);
// 插入新任务时,手动添加 id
setTodos(pre => [{ id: Date.now() + Math.random(), title: "新任务" }, ...pre]);
(三)绝对禁止:这些「假唯一」key 别碰!
- Math.random() :每次渲染都生成新值,导致所有节点被重新创建,性能比不用 key 还惨。
- 临时计算的值 (如
item.name + item.age
):如果内容变化,key 也会变,引发不必要的重渲染。
五、实战代码:对比 index 和 id 作为 key 的差异
我们用一个完整案例对比两种 key 的效果:
javascript
import { useState, useEffect } from 'react';
function App() {
const [todos, setTodos] = useState([
{ id: 1, title: "标题一" },
{ id: 2, title: "标题二" },
{ id: 3, title: "标题三" },
]);
// 5 秒后在头部插入新任务
useEffect(() => {
setTimeout(() => {
setTodos(pre => [{ id: 4, title: "标题四" }, ...pre]);
}, 5000);
}, []);
return (
<ul>
{/* 错误:用 index 当 key */}
{/* <li key={index}>{todo.title}</li> */}
{/* 正确:用 id 当 key */}
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
export default App;
- 用 index 时:插入新任务后,所有旧任务的 key (index)都会 + 1,React 认为旧节点全被删除,新节点全被创建,界面闪烁且状态丢失。
- 用 id 时:旧任务的 key 不变,React 只需在列表头部插入新节点,效率拉满!
六、面试官追问:「那什么时候可以用 index 当 key?」
划重点!只有同时满足以下两个条件时,才能用 index 当 key:
-
列表数据永不修改(如静态列表,不会增删、排序)。
-
列表项没有任何局部状态(如不含输入框、勾选框等需要维护状态的组件)。
但实际项目中,数据几乎都是动态变化的,所以 永远优先用唯一 id 当 key,这是写 React 列表的「铁律」!
七、总结:key 虽小,却是性能与稳定性的「护城河」
-
核心原理:key 是 diff 算法的标识,必须唯一且稳定。
-
避坑指南:拒绝 index,拥抱唯一 id(自带或生成)。
-
面试加分:能结合案例说明 index 导致的状态错乱和性能问题,比单纯背答案更出彩!
下次写 map
循环时,记得先问自己:「这个 key 能让 React 准确认出每个列表项吗?」养成正确的 key 使用习惯,能帮你避开 99% 的列表渲染坑,代码质量和性能双提升!