9.2 DOM 复用与 key 的作用
子节点顺序变化时,按位置比较会「就地复用」导致状态错乱;引入 key 作为虚拟节点的身份证,才能找到真正可复用的节点。
一、同位置比较的缺陷:就地复用
存在可优化的空间:如果新旧两组子节点只是顺序不同,最优的处理方式是通过移动 DOM 来完成更新。前提是能判断新旧两组子节点中确实存在可复用的节点。
按位置比较时,只认下标,不认身份,于是产生就地复用:
| 下标 | 被复用的旧节点 | 更新后内容 |
|---|---|---|
| 0 | 旧 p1 | 新序列第 0 项的内容 |
| 1 | 旧 p2 | 新序列第 1 项的内容 |
| 2 | 旧 p3 | 新序列第 2 项的内容 |
oldChildren[i].el原地接管newChildren[i]的内容,DOM 节点个数不变、位置不变,内容却与节点身份错位。
当节点内部带状态(<input> 的输入值、焦点、过渡动画等)时问题就暴露了:数据项换了位置,输入值却留在原位置的 DOM 上,出现「列表错位、输入串行」。等价于 Vue 中用下标当 key:数据移动了,DOM 复用的却是旧位置的元素。
二、key:虚拟节点的身份证
- key 是引入给 vnode 的额外标识;
- 判定规则:
type与key都相同,才认为两个虚拟节点是同一个,即可以DOM 复用; - 有了明确的对应关系,才能用「移动 DOM」代替「重建 DOM」。
三、用双层循环找可复用节点
const oldChildren = n1.children
const newChildren = n2.children
// 遍历新的 children
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
// 遍历旧的 children
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
// key 相同 → 可以复用,但仍需 patch 更新其内容
if (newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode, container)
break // 找到即可复用,必须 break
}
}
}
break不能省:同一个key只需匹配一次,否则会重复打补丁。
复杂度:外层
newChildren× 内层oldChildren,即 O(n²),这是简单 Diff 的主要代价。
四、DOM 可复用 ≠ 不需要更新
- 可复用的节点依然要先打补丁:
patch(oldVNode, newVNode, container)保证文本等内容是最新的(示例中'hello'→'world'); - 顺序变化则要靠移动真实 DOM 来修正:先用
lastIndex找出需要移动的节点(9.3),再执行移动(9.4)。
相关
- 9.1 减少 DOM 操作的性能开销:按位置比较的基本做法
- 9.3 找到需要移动的元素:用 lastIndex 判断哪些节点需要移动
- 9.4 如何移动元素:用锚点把节点移动到正确位置