9.1 减少 DOM 操作的性能开销
新旧 vnode 的
children都是一组节点时,按位置逐个比对并复用旧节点,把「全部卸载 + 全部挂载」的 6 次 DOM 操作降为 3 次。
一、Diff 算法要解决什么问题
当新旧 vnode 的子节点都是一组节点时,为了以最小的性能开销完成更新,需要比较这两组子节点;用于比较的算法就叫 Diff 算法。
朴素做法是「卸载全部旧子节点,再挂载全部新子节点」:
| 步骤 | DOM 操作次数 |
|---|---|
卸载 3 个旧 p | 3 次删除 |
挂载 3 个新 p | 3 次添加 |
| 合计 | 6 次 |
但新旧子节点其实都是 3 个 p,只是文本内容变了。最理想的更新方式是直接更新每个 p 的文本节点:一次 DOM 操作即可更新一个 p,3 个 p 共 3 次 DOM 操作,性能提升一倍。
二、同位置比较
如果认为「下标相同就是同一个节点」,就可以按下标一一对应地调用 patch:
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
// 按下标同位置比较:oldChildren[i] 更新为 newChildren[i]
for (let i = 0; i < oldChildren.length; i++) {
patch(oldChildren[i], newChildren[i], container)
}
} 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, '')
}
}
}
这套比较只认位置:
oldChildren[i]与newChildren[i]被视为同一节点。一旦子节点顺序变化,就会把 DOM 和内容对错人(见 9.2)。
三、长度不同的情况
新旧两组子节点的数量未必相同,所以要先遍历较短的那一组,再处理多出来的部分。
const oldChildren = n1.children
const newChildren = n2.children
const oldLen = oldChildren.length
const newLen = newChildren.length
// 公共长度,即较短的那一组
const commonLength = Math.min(oldLen, newLen)
// 先按位置复用,尽量多地调用 patch
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i], container)
}
if (newLen > oldLen) {
// 新子节点更长 → 挂载多出来的
for (let i = commonLength; i < newLen; i++) {
patch(null, newChildren[i], container)
}
} else if (oldLen > newLen) {
// 旧子节点更长 → 卸载多出来的
for (let i = commonLength; i < oldLen; i++) {
unmount(oldChildren[i])
}
}
| 比较结果 | 含义 | 处理 |
|---|---|---|
newLen > oldLen | 有新节点要挂载 | 从 commonLength 起 patch(null, ...) |
oldLen > newLen | 有旧节点要卸载 | 从 commonLength 起 unmount(...) |
| 相等 | 只需更新 | 只跑公共长度的 patch |
之所以遍历较短的那一组:
patch只负责复用与更新;多出来的尾部节点必然是要新增或删除的,单独处理更清晰。
四、三种 children 形态
patchChildren 按 n2.children 的类型分三条分支:
n2.children | 含义 | 处理 |
|---|---|---|
string | 文本子节点 | 旧节点若为一组节点则逐个卸载,再 setElementText |
Array | 一组子节点 | 旧节点也是一组 → Diff;否则清空容器后逐个挂载 |
| 不存在 | 无子节点 | 旧节点为一组则逐个卸载,为文本则清空 |
相关
- 9.2 DOM 复用与 key 的作用:同位置比较的缺陷,以及用 key 标识可复用节点