11.1 相同的前置元素和后置元素
快速 Diff 的第一步是预处理:先把新旧两组子节点中「相同的前置节点」和「相同的后置节点」摘出来逐个
patch,剩下的中间部分才是真正需要 Diff 的范围。
一、借鉴纯文本 Diff 的预处理思路
快速 Diff 的预处理步骤借鉴了纯文本 Diff 算法。对两段文本做 Diff 时:
| 场景 | 预处理结论 |
|---|---|
| 两段文本完全相同 | 直接结束,无须进入核心 Diff 流程 |
| 存在相同的前缀 / 后缀 | 去掉它们,只对中间不同的部分做 Diff |
| 去掉前缀后缀后一边为空 | 另一边只是在这段内容上做了插入或删除 |
TEXT1: I use vue for app development
TEXT2: I use react for app development
=> 真正需要 Diff 的只有 vue / react
TEXT1: I like you
TEXT2: I like you too
=> 去前缀后 TEXT1 为空、TEXT2 为 too,说明是新增
判断剩余的节点是「只有新增」「只有删除」还是「需要移动」,正是本章要解决的问题。
二、处理相同的前置节点
- 前置节点在新旧两组子节点中的相对位置不变,所以无须移动;但内容可能变了,仍需
patch。 - 建立索引
j = 0指向两组子节点的开头,while循环让j递增,直到遇到 key 不同的节点为止。
// 处理相同的前置节点
let j = 0
let oldVNode = oldChildren[j]
let newVNode = newChildren[j]
// 向后遍历,直到遇到拥有不同 key 值的节点为止
while (oldVNode?.key === newVNode?.key) {
// key 相同才复用,内容差异交给 patch 处理
patch(oldVNode, newVNode, container)
j++
oldVNode = oldChildren[j]
newVNode = newChildren[j]
// 越界保护:两组节点数量相同时不要继续访问
if (!oldVNode && !newVNode) break
}
三、处理相同的后置节点
- 需要
oldEnd与newEnd两个索引,分别指向新旧两组子节点的最后一个节点。 - 从后向前遍历,直到遇到 key 不同的节点为止。
// 处理相同的后置节点
let oldEnd = oldChildren.length - 1
let newEnd = newChildren.length - 1
oldVNode = oldChildren[oldEnd]
newVNode = newChildren[newEnd]
// 从后向前遍历,直到遇到拥有不同 key 值的节点为止
while (oldVNode.key === newVNode.key) {
patch(oldVNode, newVNode, container)
oldEnd--
newEnd--
oldVNode = oldChildren[oldEnd]
newVNode = newChildren[newEnd]
}
四、预处理后的三种情况
预处理结束后,剩余未处理的区间是:旧节点 j -> oldEnd、新节点 j -> newEnd。
| 条件 | 含义 | 处理方式 |
|---|---|---|
j > oldEnd && j <= newEnd | 旧节点用完,新节点还有剩余 | 剩余新节点全是新增节点,挂载 |
j > newEnd && j <= oldEnd | 新节点用完,旧节点还有剩余 | 剩余旧节点全是待删除节点,卸载 |
| 其它 | 两边都还有剩余 | 非理想情况,需要 DOM 移动 |
新增节点:
if (j > oldEnd && j <= newEnd) {
// 锚点:新节点区间之后「第一个已处理节点」的真实 DOM
const anchorIndex = newEnd + 1
const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null
while (j <= newEnd) {
patch(null, newChildren[j++], container, anchor)
}
}
删除节点:
else if (j > newEnd && j <= oldEnd) {
// j -> oldEnd 之间的旧节点全部卸载
while (j <= oldEnd) {
unmount(oldChildren[j++])
}
}
五、示例
| 旧子节点 | 新子节点 | 预处理结果 | 结论 |
|---|---|---|---|
1 2 3 | 1 4 2 3 | 前置复用 1(j=1);后置复用 3、2(oldEnd=0,newEnd=1) | j > oldEnd && j <= newEnd,把 key4 挂到 key2 的 DOM 前 |
1 2 3 | 1 3 | 前置复用 1(j=1);后置复用 3(oldEnd=1,newEnd=0) | j > newEnd && j <= oldEnd,卸载 key2 |
六、易错点
比较的是 key 而不是节点内容:
key相同才复用,内容差异由patch负责更新。 新增节点的锚点取newEnd + 1位置节点的真实 DOM;该位置不存在时传null,表示追加到末尾。 两个while都要做越界保护(oldVNode && ...判空、j >= 长度时提前return),否则两组节点数量不一致时会访问到undefined。
相关
- 11.2 判断是否需要进行 DOM 移动操作:预处理后两边都还有剩余时,如何用
source数组判断是否需要移动。