React 中的 key 有什么作用?
一、是什么
key 是 React 用于识别列表中每个元素身份的特殊属性。它帮助 React 在 Reconciliation(协调)过程中高效地判断哪些元素发生了变化、被添加或被移除。
key 不是传递给组件的 prop(子组件无法通过 props.key 获取),它仅供 React 内部使用。
function TodoList({ todos }: { todos: Todo[] }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
二、key 在 Reconciliation 中的作用
React 采用 Diff 算法比较前后两棵虚拟 DOM 树来计算最小更新操作。对于列表元素的 Diff,React 依赖 key 来建立新旧元素之间的对应关系。
没有 key 的情况
如果没有提供 key,React 默认按照索引逐个比较。当列表顺序改变时,这种方式会导致大量不必要的 DOM 更新。
假设列表从 [A, B, C] 变为 [C, A, B]:
无 key(按索引比较):
位置 0:A → C(更新内容)
位置 1:B → A(更新内容)
位置 2:C → B(更新内容)
→ 3 次 DOM 更新
有 key(按 key 匹配):
key="A":位置 0 → 位置 1(移动)
key="B":位置 1 → 位置 2(移动)
key="C":位置 2 → 位置 0(移动)
→ DOM 移动操作,无内容更新
Diff 过程图解
React 对列表进行 Diff 时的步骤:
- 用旧列表的 key 建立 Map:
{ key → Fiber }
- 遍历新列表,用每个元素的 key 在 Map 中查找
- 找到了:复用旧 Fiber,标记为移动或更新
- 没找到:创建新 Fiber
- Map 中剩余的 key:标记为删除
// 第一次渲染
const listV1 = [
{ id: 'a', text: '学习 React' },
{ id: 'b', text: '学习 TypeScript' },
{ id: 'c', text: '学习 Next.js' },
];
// 第二次渲染:在头部插入一条
const listV2 = [
{ id: 'd', text: '学习 Rust' }, // 新增
{ id: 'a', text: '学习 React' }, // 复用
{ id: 'b', text: '学习 TypeScript' }, // 复用
{ id: 'c', text: '学习 Next.js' }, // 复用
];
有正确的 key 时,React 知道 a、b、c 三个元素没有变化,只需要创建 d 并将其插入头部。
三、为什么不应该使用 index 作为 key
使用数组索引作为 key 在列表会重排、插入或删除时会导致严重问题。
问题一:状态错乱
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: '任务 A' },
{ id: 2, text: '任务 B' },
{ id: 3, text: '任务 C' },
]);
const addToTop = () => {
setTodos([{ id: Date.now(), text: '新任务' }, ...todos]);
};
return (
<div>
<button onClick={addToTop}>在顶部添加</button>
<ul>
{todos.map((todo, index) => (
// ❌ 使用 index 作为 key
<li key={index}>
<TodoItem text={todo.text} />
</li>
))}
</ul>
</div>
);
}
function TodoItem({ text }: { text: string }) {
const [checked, setChecked] = useState(false);
return (
<label>
<input
type="checkbox"
checked={checked}
onChange={e => setChecked(e.target.checked)}
/>
{text}
</label>
);
}
假设用户勾选了"任务 A"(index=0),然后点击"在顶部添加"。新列表中 index=0 变成了"新任务",但 React 认为 key=0 的元素没变(还是同一个位置),于是复用了之前的组件实例——导致"新任务"显示为已勾选,而"任务 A"变成未勾选。
问题二:性能退化
// 在列表头部插入元素
// 旧列表 key: [0, 1, 2]
// 新列表 key: [0, 1, 2, 3]
//
// 使用 index 作为 key:
// key=0: 旧元素 A → 新元素 D(更新内容)
// key=1: 旧元素 B → 新元素 A(更新内容)
// key=2: 旧元素 C → 新元素 B(更新内容)
// key=3: 不存在 → 新元素 C(创建)
// → 3 次不必要的更新 + 1 次创建
// 使用 id 作为 key:
// key='d': 不存在 → 新元素 D(创建)
// key='a': 存在,内容相同(跳过)
// key='b': 存在,内容相同(跳过)
// key='c': 存在,内容相同(跳过)
// → 仅 1 次创建
何时可以使用 index
以下条件同时满足时,使用 index 作为 key 是安全的:
- 列表是静态的,不会重排、插入或删除
- 列表项没有自己的内部状态
- 列表项没有使用非受控的输入元素
// ✅ 静态导航菜单,使用 index 是安全的
const menuItems = ['首页', '关于', '联系'];
function Nav() {
return (
<nav>
{menuItems.map((item, index) => (
<a key={index} href={`/${item}`}>{item}</a>
))}
</nav>
);
}
四、key 的最佳实践
使用稳定、唯一的标识符
// ✅ 使用数据库 ID
{users.map(user => <UserCard key={user.id} user={user} />)}
// ✅ 使用 UUID 或其他唯一标识
{messages.map(msg => <Message key={msg.uuid} message={msg} />)}
// ✅ 组合多个字段形成唯一 key
{items.map(item => (
<Row key={`${item.category}-${item.name}`} item={item} />
))}
避免使用随机值
// ❌ 每次渲染都生成新的 key,导致所有子组件被销毁重建
{items.map(item => (
<div key={Math.random()}>{item.text}</div>
))}
// ❌ crypto.randomUUID() 同理
{items.map(item => (
<div key={crypto.randomUUID()}>{item.text}</div>
))}
五、利用 key 重置组件状态
key 不仅用于列表,还可以用来强制重置组件的内部状态。当 key 变化时,React 会销毁旧组件并创建新组件。
切换用户时重置表单
function UserEditor({ userId }: { userId: string }) {
return (
<div>
<h2>编辑用户</h2>
{/* key 变化时,EditForm 会被完全重置 */}
<EditForm key={userId} userId={userId} />
</div>
);
}
function EditForm({ userId }: { userId: string }) {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
useEffect(() => {
fetchUser(userId).then(user => {
setName(user.name);
setEmail(user.email);
});
}, [userId]);
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
</form>
);
}
重置动画状态
function AnimatedNotification({ message, id }: { message: string; id: number }) {
return (
<div key={id} className="notification fade-in">
{message}
</div>
);
}
每次 id 变化时,整个 DOM 元素会被重新创建,CSS 动画会重新播放。
强制重新挂载第三方组件
function ChartWrapper({ config }: { config: ChartConfig }) {
const configKey = JSON.stringify(config);
return (
<div>
{/* 配置结构性变化时,重新初始化图表 */}
<ThirdPartyChart key={configKey} config={config} />
</div>
);
}
六、key 与 Fragment
当需要在列表中返回多个元素时,可以使用带 key 的 Fragment:
function Glossary({ items }: { items: { term: string; description: string }[] }) {
return (
<dl>
{items.map(item => (
<Fragment key={item.term}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</Fragment>
))}
</dl>
);
}
注意,简写语法 <>...</> 不支持 key 属性,必须使用 <Fragment key={...}>。
七、React 18/19 中 key 的注意事项
Strict Mode 双重渲染
React 18 的严格模式下,组件会在开发环境中渲染两次来帮助发现副作用问题。如果 key 不正确导致状态错乱,这一机制会更容易暴露问题。
Concurrent Features
React 18 的并发特性(如 useTransition、useDeferredValue)可能会在后台准备新的 UI,如果 key 设置不当导致状态复用问题,在并发模式下会更加明显。
function SearchResults({ query }: { query: string }) {
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState<Item[]>([]);
useEffect(() => {
startTransition(() => {
fetchResults(query).then(setResults);
});
}, [query]);
return (
<div style={{ opacity: isPending ? 0.7 : 1 }}>
{results.map(item => (
<ResultCard key={item.id} item={item} />
))}
</div>
);
}
八、总结
key 看似简单,但深入理解它在 Reconciliation 中的作用机制,能够帮助开发者编写出性能更优、行为更可预测的 React 应用。