9.6 移除不存在的元素
基本更新结束后,再遍历旧 children,凡是在新 children 中找不到相同 key 的节点,就是要删除的节点。
一、更新完成后做一次清理
// 上一步的更新操作完成后,遍历旧的一组子节点
for (let i = 0; i < oldChildren.length; i++) {
const oldVNode = oldChildren[i]
// 拿旧子节点去新的一组子节点中寻找具有相同 key 值的节点
const has = newChildren.find(vnode => vnode.key === oldVNode.key)
if (!has) {
// 没有找到 → 该节点已经不存在,需要卸载
unmount(oldVNode)
}
}
- 位置:紧跟在「遍历新 children 做更新」的循环之后,仍在
Array.isArray(n1.children)这条分支内; newChildren.find(...)返回第一个匹配项或undefined,据此判断节点是否还存在;- 真正的删除由
unmount完成:它通过vnode.el.parentNode.removeChild(vnode.el)摘掉真实 DOM(若vnode.type === Fragment则递归卸载其children)。
二、推演:old [1,2,3] → new [3,1,4]
| 旧节点 | 出现在新 children 中? | 动作 |
|---|---|---|
| p1 | 是 | 保留(更新/移动) |
| p2 | 否 | unmount 删除 |
| p3 | 是 | 保留(更新/移动) |
三、完整实现(合并 9.2–9.6)
function patchChildren(n1, n2, container) {
if (typeof n2.children === 'string') {
if (Array.isArray(n1.children)) {
n1.children.forEach((c) => unmount(c))
}
setElementText(container, n2.children)
} else if (Array.isArray(n2.children)) {
if (Array.isArray(n1.children)) {
const oldChildren = n1.children
const newChildren = n2.children
// 寻找过程中遇到的最大索引值
let lastIndex = 0
// 遍历新的 children
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
let find = false // 是否找到可复用节点
// 遍历旧的 children
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
// key 相同 → 可复用,先 patch 更新内容
if (newVNode.key === oldVNode.key) {
find = true
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 索引变小 → 真实 DOM 需要移动
const prevVNode = newChildren[i - 1]
if (prevVNode) {
const anchor = prevVNode.el.nextSibling
insert(newVNode.el, container, anchor)
}
} else {
lastIndex = j
}
break
}
}
// 旧 children 中没找到 → 新增节点,挂载到正确位置
if (!find) {
const prevVNode = newChildren[i - 1]
let anchor = null
if (prevVNode) {
anchor = prevVNode.el.nextSibling
} else {
anchor = container.firstChild
}
patch(null, newVNode, container, anchor)
}
}
// 更新结束,清理旧 children 中已不存在的节点
for (let i = 0; i < oldChildren.length; i++) {
const oldVNode = oldChildren[i]
const has = newChildren.find(vnode => vnode.key === oldVNode.key)
if (!has) {
unmount(oldVNode)
}
}
} else {
setElementText(container, '')
n2.children.forEach(c => patch(null, c, container))
}
} else {
if (Array.isArray(n1.children)) {
n1.children.forEach(c => unmount(c))
} else if (typeof n1.children === 'string') {
setElementText(container, '')
}
}
}
四、简单 Diff 的复杂度与缺点
| 维度 | 简单 Diff |
|---|---|
| 查找可复用节点 | 新 × 旧 双层循环,O(n²) |
| 移动判断 | 仅靠 lastIndex,只保证「字典序靠后」的相对顺序 |
| 移动次数 | 非最优,最坏情况下移动次数远多于理论最小值 |
因此第 10 章的双端 Diff 算法、第 11 章的快速 Diff 算法会分别给出更优的更新方案。
相关
- 9.5 添加新元素:用 find 判断并挂载新增节点
- 9.1 减少 DOM 操作的性能开销:Diff 算法的起点与三种 children 分支