为什么 id 变了 key 才变?
为什么 id 没变,内容变了也能更新?
列表里第一项 key 变了,第二项会受牵连吗?
key 写在 div 上和写在组件上有区别吗?
如果你也被这些问题困扰过,别怕,你不是一个人。
本文会用最通俗的比喻 + 最清晰的拆解,帮你彻底搞懂 key 在虚拟 DOM diff 中的真正角色。
一、key 是“身份证号”,不是“名牌”
先看最基础的例子:
const list = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
];
list.map(item => <div key={item.id}>{item.name}</div>)
这里的 key={item.id},相当于给每个 <div> 发了一张 唯一的身份证。
diff 算法通过这个身份证号来判断:“这个元素之前出现过吗?”
关键点
身份证号标识的是 “这个人”,而不是 “他叫什么名字”。
当 id=1 的 name 从“张三”变成“张老三”时:
- ✅ 身份证号没变 → 算法知道:还是同一个人(同一个虚拟节点)
- ✅ 名字变了 → 算法会更新这个人身上的 “名牌”(DOM 文本内容)
所以,你最初的困惑就解开了:
id 不变,名称变了,为什么能准确更新?
因为 key 只管“是谁”,不管“叫什么”。名称是属性(props),属性变了,框架自然会更新视图。
二、深入组件更新:实例、属性、渲染,别搞混了
很多同学分不清这三个概念,我们用 “收件箱” 来比喻:
- 📦 组件实例 = 你家的收件箱(固定在门口)
- 📨 属性(props) = 每天收到的信(内容可以不同)
- 👀 渲染 = 你打开收件箱,阅读信的过程
场景 1:id 没变,内容变了(张三 → 李四)
- 实例:收件箱没变(组件实例保留)
- 属性:今天收到的信里,名字换成了“李四”
- 渲染:你打开信,看到新名字,大脑更新记忆(视图更新)
✅ 结果:实例复用,属性更新,视图随之变化。
场景 2:id 没变,内容也没变(还是“张三”)
- 实例:收件箱没变
- 属性:今天收到的信和昨天一模一样
- 渲染:你打开信,发现内容没变,可能只是瞥一眼(不一定重新执行渲染函数)
这里不同框架有差异:
- React(默认):即使信的内容一样,只要父组件因为别的原因重新渲染了,React 会“习惯性”地让所有子组件重新执行一遍渲染函数,但最终因为内容没变,不会更新真实 DOM。
- React 优化后:用
React.memo包裹组件后,如果信的内容没变,也会跳过重新渲染。 - Vue:Vue 能精确追踪依赖,如果信的内容没变,它根本不会触发子组件重新渲染。
💡 关键结论
第二个组件会不会重新执行渲染?
在 React 默认情况下会(但 DOM 不更新),在 Vue 下不会。
不过放心,最终页面上显示的内容一定是正确的。
三、列表更新的连锁反应
现在考虑一个有两个数据的列表:
const [data, setData] = useState([
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
]);
某次操作后,第一个数据的 id 变了(比如 1 → 3),第二个数据没变:
[
{ id: 3, name: '张三' }, // id 变了,但名字还是张三(假设业务上这个人变了)
{ id: 2, name: '李四' } // 完全没变
]
渲染的 JSX:
data.map(item => <MyComponent key={item.id} data={item} />)
发生了什么?
- 第一个组件:key 从 1 变成 3 → 算法认为旧的组件消失,新的组件出现 → 销毁旧组件,创建新组件。
- 第二个组件:key=2 没变 → 组件实例保留。
- 父组件重新渲染:整个列表重新执行渲染。
第二个组件会重新渲染吗?
- 在 React 中:父组件重新渲染时,默认所有子组件都会重新执行渲染函数,包括第二个组件。虽然它的 props 内容没变(仍是 { id:2, name:'李四' }),但渲染函数会跑一遍。如果没用 React.memo,它就会重新执行。
- 在 Vue 中:因为传给第二个组件的 props 没有变化,Vue 的响应式系统会检测到依赖没变,所以不会重新渲染。
🎯 结论
key 没变保证了第二个组件的实例不被销毁,但父组件的重新渲染可能会“波及”它(React 默认行为,如果你希望第二个组件完全不受影响,可以用 React.memo 或 shouldComponentUpdate 优化)。
四、虚拟 DOM diff 的三步曲
通过上面的分析,我们可以总结出 diff 算法的核心流程:
1. 比类型
先看两个节点是不是同一种元素(比如 <div> 和 <span>)。
- ❌ 类型不同 → 直接销毁重建。
2. 比 key
如果类型相同,再看 key 是否一致。
- ❌ key 不同 → 也销毁重建(认为不是同一个节点)。
3. 比属性 / 内容
如果类型和 key 都相同,说明节点可复用。
- ✅ 此时才深入对比属性有没有变化,子节点有没有变化,只更新变化的部分。
总结
先通过 key 确定“人在不在”,再比对内容决定“哪里要改”。
五、key 放在 div 上和组件上有区别吗?
很多同学发现我前面经常在说“组件”,但自己平时是把 key 写在 <div> 上的,这两者有区别吗?
核心结论
本质完全一样。
因为无论是 <div> 还是 <MyComponent>,在虚拟 DOM 中都是 节点。
key 是附加给节点的标识,diff 算法一视同仁。
区别仅在于节点类型
<div>是原生 DOM 节点:更新时,框架直接操作真实 DOM 的属性。<MyComponent>是组件节点:更新时,框架会触发组件实例的更新流程(调用 render、执行 hooks 等),然后再处理组件内部的 DOM。
但 key 的角色是一样的:帮助判断这个节点是否需要复用。
所以放心大胆地在列表的每个元素上使用 key,无论它是 div 还是组件,原理都一样。
六、总结:记住两句话就够了
1. key 是稳定的身份标识,不是内容描述
- id 不变 → 同一个节点 → 内容变了会更新
- id 变了 → 不同节点 → 旧节点销毁,新节点创建
2. key 的作用是“定人”,更新与否是“做事”
- key 只管节点是否复用
- 节点复用后,属性和内容的更新由框架的响应式系统或渲染机制负责
理解了这两点,你在写列表时就能自信地选择稳定的 id 作为 key,再也不用担心更新错乱了。
如果你觉得本文对你有帮助,欢迎点赞、收藏,也欢迎在评论区留下你的疑问,我们一起讨论~