第十章 双端Diff算法(vue2)
10.1 双端比较的原理
- 第一步:比较旧的一组子节点中第一个子节点
p-1
与新的一组子节点中的第一个子节点p-4
,由于两者的key
值不同,因此不相同,不可复用,于是什么都不做。 - 第二步:比较旧的一组子节点中的最后一个子节点
p-4
与新的一组子节点中最后一个子节点p-3
,由于两者key
值不同,因此不同,不可复用,于是什么都不做。 - 第三步:比较旧的一组子节点中第一个子节点
p-1
与新的一组子节点中的最后一个子节点p-3
,由于两者key
值不同,因此不同,不可复用,于是什么都不做 - 第四步:比较旧的一组子节点中最后一个子节点
p-4
与新的一组子节点中的第一个子节点p-4
,由于它们key
值相同,因此可以进行DOM复用
- DOM移动:将索引
oldEndIdx
指向的虚拟节点对应的真是DOM移动到oldStartIdx
指向的虚拟节点对应的真实DOM前面 - 移动DOM完成后,更新索引值,并指向下一个位置
- 重复第一步,
p-1
与p-2
不同不做动作 - 重复第二步,
p-3
相同,可复用。另外,由于两者皆处于尾部,因此不需要对真实DOM进行移动,只需要打补丁即可 - 更新索引值,并指向下一个位置
- 重复第一步,
p-1
与p-2
不同不做动作 - 重复第二步,
p-2
与p-1
不同不做动作 - 重复第三步,
p-1
相同,可以复用 - 在第三步中找到相同节点,这说明
p-1
原本是头部节点,但在新顺序中,它变成了尾部节点。因此,需要将p-1
对应的真实DOM移动到旧的一组节点的尾部节点p-2
对应的真实DOM后面
- 如果旧的一组子节点的头部节点与新的一组子节点的尾部节点匹配,说明旧节点所对应的真实DOM节点需要移动到尾部
- 因此,需要获取当前尾部节点的下一个兄弟节点作为锚点,即
oldEndVNode.el.nextSibling
- 更新索引值,并指向下一个位置
- 重复第一步,
p-2
相同,可复用。但两者在新旧两组子节点中都是头部节点,因此不需要移动,只需要调用patch
函数打补丁即可
10.3 非理想状况的处理
- 双端diff四个步骤都无法命中的情况下,拿新的一组子节点中的头部节点去旧的一组节点中寻找
- 节点
p-2
原本不是头部节点,但在更新之后成为了头部节点。所以需要将p-2
对应的真实DOM移动到当前旧的一组子节点的头部节点p-1
对应的真实DOM节点之前
01 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
02 if (oldStartVNode.key === newStartVNode.key) {
03
04 } else if (oldEndVNode.key === newEndVNode.key) {
05
06 } else if (oldStartVNode.key === newEndVNode.key) {
07
08 } else if (oldEndVNode.key === newStartVNode.key) {
09
10 } else {
11
12 const idxInOld = oldChildren.findIndex(
13 node => node.key === newStartVNode.key
14 )
15
16 if (idxInOld > 0) {
17
18 const vnodeToMove = oldChildren[idxInOld]
19
20 patch(vnodeToMove, newStartVNode, container)
21
22 insert(vnodeToMove.el, container, oldStartVNode.el)
23
24 oldChildren[idxInOld] = undefined
25
26 newStartVNode = newChildren[++newStartIdx]
27 }
28 }
29 }
10.4 添加新元素
情况一
01 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
02
03 if (!oldStartVNode) {
04 oldStartVNode = oldChildren[++oldStartIdx]
05 } else if (!oldEndVNode) {
06 oldEndVNode = newChildren[--oldEndIdx]
07 } else if (oldStartVNode.key === newStartVNode.key) {
08
09 } else if (oldEndVNode.key === newEndVNode.key) {
10
11 } else if (oldStartVNode.key === newEndVNode.key) {
12
13 } else if (oldEndVNode.key === newStartVNode.key) {
14
15 } else {
16 const idxInOld = oldChildren.findIndex(
17 node => node.key === newStartVNode.key
18 )
19 if (idxInOld > 0) {
20 const vnodeToMove = oldChildren[idxInOld]
21 patch(vnodeToMove, newStartVNode, container)
22 insert(vnodeToMove.el, container, oldStartVNode.el)
23 oldChildren[idxInOld] = undefined
24 } else {
25
26 patch(null, newStartVNode, container, oldStartVNode.el)
27 }
28 newStartVNode = newChildren[++newStartIdx]
29 }
30 }
情况二
- 当更新完毕后,由于变量
oldStartIdx
的值大于oldEndIdx
满足终止条件,因此停止更新。但是节点p-4
在更新中被遗漏
01 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
02
03 }
04
05
06 if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
07
08 for (let i = newStartIdx; i <= newEndIdx; i++) {
09 patch(null, newChildren[i], container, oldStartVNode.el)
10 }
11 }
newStartIdx
和newEndIdx
之间的节点都应该被添加
10.5 移除不存在的元素
- 此时变量
newStartIdx
的值大于变量newEndIdx
的值满足更新停止条件,但是节点中存在未被处理的节点
01 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
02
03 }
04
05 if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
06
07
08 } else if (newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {
09
10 for (let i = oldStartIdx; i <= oldEndIdx; i++) {
11 unmount(oldChildren[i])
12 }
13 }
oldStartIdx
和oldEndIdx
区域之间的节点都应该被卸载