别再疑惑 key 了!一篇彻底搞懂虚拟 DOM 中的“身份ID”

0 阅读6分钟

为什么 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 没变,内容变了(张三 → 李四)

  1. 实例:收件箱没变(组件实例保留)
  2. 属性:今天收到的信里,名字换成了“李四”
  3. 渲染:你打开信,看到新名字,大脑更新记忆(视图更新)

结果:实例复用,属性更新,视图随之变化。

场景 2:id 没变,内容也没变(还是“张三”)

  1. 实例:收件箱没变
  2. 属性:今天收到的信和昨天一模一样
  3. 渲染:你打开信,发现内容没变,可能只是瞥一眼(不一定重新执行渲染函数)

这里不同框架有差异:

  • 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} />)

发生了什么?

  1. 第一个组件:key 从 1 变成 3 → 算法认为旧的组件消失,新的组件出现 → 销毁旧组件,创建新组件。
  2. 第二个组件:key=2 没变 → 组件实例保留。
  3. 父组件重新渲染:整个列表重新执行渲染。

第二个组件会重新渲染吗?

  • 在 React 中:父组件重新渲染时,默认所有子组件都会重新执行渲染函数,包括第二个组件。虽然它的 props 内容没变(仍是 { id:2, name:'李四' }),但渲染函数会跑一遍。如果没用 React.memo,它就会重新执行。
  • 在 Vue 中:因为传给第二个组件的 props 没有变化,Vue 的响应式系统会检测到依赖没变,所以不会重新渲染。

🎯 结论

key 没变保证了第二个组件的实例不被销毁,但父组件的重新渲染可能会“波及”它(React 默认行为,如果你希望第二个组件完全不受影响,可以用 React.memoshouldComponentUpdate 优化)。


四、虚拟 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,再也不用担心更新错乱了。


如果你觉得本文对你有帮助,欢迎点赞、收藏,也欢迎在评论区留下你的疑问,我们一起讨论~