面试官:为什么 React 的 map 渲染需要 key?为什么不能用 index 作为 key?
我:emmm...因为...性能优化?
面试官:具体怎么优化的?
我:...(内心OS:完蛋,又要挂了)
相信很多小伙伴都遇到过类似的灵魂拷问。今天咱们就用一个简单的 Todo 应用来彻底搞懂 React 中 key 的奥秘!
从一个简单的例子说起
先来看看这个经典的 Todo 列表:
import { useState, useEffect } from 'react'
function App() {
const [todos, setTodos] = useState([
{ id: 1, title: '标题一' },
{ id: 2, title: '标题二' },
{ id: 3, title: '标题三' },
])
useEffect(() => {
setTimeout(() => {
// 场景一:修改某一项内容
// setTodos(prev => prev.map(todo => {
// if (todo.id === 1) return {
// ...todo,
// title: '标题-改-'
// }
// return todo
// }))
// 场景二:在数组开头插入新元素
setTodos(prev => [
{ id: 4, title: '标题四' },
...prev
])
}, 1000)
}, [])
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
看起来很简单对吧?但这里面藏着 React 性能优化的大学问!
React 的 Diff 算法是怎么工作的?
当 todos 状态发生变化时,React 并不会傻乎乎地把整个列表重新渲染一遍。它会:
- 生成新的虚拟 DOM 树:基于新的
todos数组 - 与旧的虚拟 DOM 进行对比:这就是传说中的 diff 算法
- 计算出最小的变更:只更新真正需要变化的部分
- 应用到真实 DOM:减少重绘重排的开销
这个过程就像是找茬游戏,React 需要快速找出新旧两张图片的差异。
为什么 key 这么重要?
没有 key 的情况
如果我们不提供 key,React 只能按照索引顺序来比较元素:
// 旧状态
[
<li>标题一</li>, // index: 0
<li>标题二</li>, // index: 1
<li>标题三</li> // index: 2
]
// 在开头插入新元素后
[
<li>标题四</li>, // index: 0 ❌ React以为这是"标题一"变成了"标题四"
<li>标题一</li>, // index: 1 ❌ React以为这是"标题二"变成了"标题一"
<li>标题二</li>, // index: 2 ❌ React以为这是"标题三"变成了"标题二"
<li>标题三</li> // index: 3 ✅ 新增的元素
]
结果就是 React 以为前面三个元素都变了,需要更新 3 个 DOM 节点!
有了唯一 key 的情况
当我们使用唯一的 id 作为 key 时:
// 旧状态
[
<li key="1">标题一</li>,
<li key="2">标题二</li>,
<li key="3">标题三</li>
]
// 在开头插入新元素后
[
<li key="4">标题四</li>, // ✅ 新元素,需要创建
<li key="1">标题一</li>, // ✅ 找到了!位置变了但内容没变
<li key="2">标题二</li>, // ✅ same here
<li key="3">标题三</li> // ✅ same here
]
React 通过 key 就能准确识别:
key="4"是新元素,需要创建并插入key="1",key="2",key="3"只是位置变了,直接移动就行
为什么不能用 index 作为 key?
很多新手会这样写:
{todos.map((todo, index) => (
<li key={index}>{todo.title}</li>
))}
这在什么情况下会出问题呢?
问题场景:在数组开头插入元素
// 原始数组
[
{ id: 1, title: '标题一' }, // key: 0
{ id: 2, title: '标题二' }, // key: 1
{ id: 3, title: '标题三' } // key: 2
]
// 在开头插入新元素后
[
{ id: 4, title: '标题四' }, // key: 0 ❌
{ id: 1, title: '标题一' }, // key: 1 ❌
{ id: 2, title: '标题二' }, // key: 2 ❌
{ id: 3, title: '标题三' } // key: 3 新增
]
看到问题了吗?所有元素的 key 都变了!React 会认为:
key=0的元素从"标题一"变成了"标题四"key=1的元素从"标题二"变成了"标题一"key=2的元素从"标题三"变成了"标题二"- 新增了一个
key=3的元素
结果就是触发了 3 次不必要的 DOM 更新!
性能优化的本质
使用正确的 key 能带来什么好处?
- 减少 DOM 操作:只操作真正变化的元素
- 避免重绘重排:不必要的样式计算和布局计算
- 保持组件状态:如果列表项是复杂组件,还能保持其内部状态
- 提升用户体验:动画效果更流畅,响应更快
实践总结
-
总是使用唯一且稳定的 key
// ✅ 好的做法 {todos.map(todo => ( <li key={todo.id}>{todo.title}</li> ))} -
避免使用 index 作为 key(除非列表完全静态)
// ❌ 避免这样做 {todos.map((todo, index) => ( <li key={index}>{todo.title}</li> ))} -
如果数据没有唯一标识,可以使用一些技巧
// 权宜之计,但不推荐 {todos.map((todo, index) => ( <li key={`${todo.title}-${index}`}>{todo.title}</li> ))}
写在最后
React 的 diff 算法虽然已经很智能了,但它不是魔法,需要我们给它一些"提示"。key 就是这样一个重要的提示,它帮助 React 更准确地识别元素的变化,从而做出最优的更新策略。
下次面试官再问你 key 的问题,你就可以自信地说:"key 是 React diff 算法的重要依据,它帮助 React 准确识别列表元素的变化,避免不必要的 DOM 操作,提升应用性能!"
性能优化不是玄学,而是对原理的深度理解!