剖析Vue3.0 diff算法

diff算法的核心就是子节点之间的比对,主要分为两种情况(子节点有key和无key的情况)

if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
    patchKeyedChildren(
        c1 as VNode[],
        c2 as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
    )
    return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
    patchUnkeyedChildren(
        c1 as VNode[],
        c2 as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
    )
    return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

一.无Key的情况

当元素无key时,我们希望尽可能复用老节点

  const patchUnkeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    c1 = c1 || EMPTY_ARR
    c2 = c2 || EMPTY_ARR
    const oldLength = c1.length // 老节点长度
    const newLength = c2.length // 新节点长度
    // 计算能复用的节点
    const commonLength = Math.min(oldLength, newLength)
    let i
    for (i = 0; i < commonLength; i++) {
      const nextChild = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      patch(
        c1[i],
        nextChild,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
    // 老元素多余新元素,将老元素卸载掉
    if (oldLength > newLength) { 
      // remove old
      unmountChildren(c1, parentComponent, parentSuspense, true, commonLength)
    } else {
      // 将多余的新元素挂载到老节点上
      mountChildren(
        c2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized,
        commonLength
      )
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

二.有key的情况

diff算法的核心还是有key的情况的比对

let i = 0 // 开始索引
const l2 = c2.length;
let e1 = c1.length - 1 // 老节点最后的索引
let e2 = l2 - 1  // 新节点最后的索引
1
2
3
4

1.从头开始比对

while (i <= e1 && i <= e2) {
    const n1 = c1[i]
    const n2 = (c2[i] = optimized
    ? cloneIfMounted(c2[i] as VNode)
    : normalizeVNode(c2[i]))
    if (isSameVNodeType(n1, n2)) { // 如果是相同节点就做patch
    patch(
        n1,
        n2,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
    )
    } else { // 否则跳出循环
        break
    }
    i++
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

2.从尾部开始比对

while (i <= e1 && i <= e2) {
    const n1 = c1[e1]
    const n2 = (c2[e2] = optimized
    ? cloneIfMounted(c2[e2] as VNode)
    : normalizeVNode(c2[e2]))
    if (isSameVNodeType(n1, n2)) { 
    patch(
        n1,
        n2,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
    )
    } else { // 非相同节点跳出循环
        break
    }
    e1-- // 移动指针
    e2--
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

3.同序列挂载

if (i > e1) {
    if (i <= e2) {
    const nextPos = e2 + 1
    const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
    while (i <= e2) { // 新节点多出来的插入到老节点中
        patch(
            null,
            (c2[i] = optimized
                ? cloneIfMounted(c2[i] as VNode)
                : normalizeVNode(c2[i])),
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG
        )
        i++
    }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

新的节点个数多余老节点,(头部插入、尾部插入)

4.同序列卸载

else if (i > e2) {
    while (i <= e1) {
        unmount(c1[i], parentComponent, parentSuspense, true)
        i++
    }
}
1
2
3
4
5
6

老的节点个数多余新节点,(头部删除、尾部删除)

5.未知序列

5.1 根据key创建映射表

const s1 = i // 确定老节点开始位置
const s2 = i // 确定新节点开始位置

const keyToNewIndexMap: Map<string | number, number> = new Map()
for (i = s2; i <= e2; i++) {
    const nextChild = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
    if (nextChild.key != null) { // 创建key的映射表
        keyToNewIndexMap.set(nextChild.key, i)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

在这里我们需要将未patch的新节点根据key和索引制作成映射表,为后续复用逻辑做准备

5.2 循环老节点依次进行patch

let j
let patched = 0; // 标记已经patched个数
const toBePatched = e2 - s2 + 1 // 标记还需要patched的个数
let moved = false // 是否需要移动 
let maxNewIndexSoFar = 0 // 临时标记
const newIndexToOldIndexMap = new Array(toBePatched) // 根据需要patched的个数创建数组
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0;
for (i = s1; i <= e1; i++) {
    const prevChild = c1[i]
    if (patched >= toBePatched) {
        // all new children have been patched so this can only be a removal
        unmount(prevChild, parentComponent, parentSuspense, true)
        continue
    }
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

如果新节点没有需要patched,则循环老节点,将老节点依次进行卸载

let newIndex
if (prevChild.key != null) { // 通过老节点的key去新节点中找到对应索引
    newIndex = keyToNewIndexMap.get(prevChild.key)
}
if (newIndex === undefined) { // 如果索引不存在则直接删除老节点
    unmount(prevChild, parentComponent, parentSuspense, true)
} 
1
2
3
4
5
6
7

for (j = s2; j <= e2; j++) {
    if (
        newIndexToOldIndexMap[j - s2] === 0 &&
        isSameVNodeType(prevChild, c2[j] as VNode)
    ) {
        newIndex = j
        break
    }
}
1
2
3
4
5
6
7
8
9

如果老节点无key,则会找到对应新节点看类型是否相同,如果类型相同则进行patch

newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
    maxNewIndexSoFar = newIndex
} else {
    moved = true
}
patch(
    prevChild,
    c2[newIndex] as VNode,
    container,
    null,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
)
patched++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

标记已经patched过的元素,用于标记最长稳定序列,并且标记元素是否需要移动,最终值为0的则意味着元素没有被patch过

5.3 移动和挂载

const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap) // 获取最长稳定序列
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
for (i = toBePatched - 1; i >= 0; i--) { 
const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode
const anchor =
    nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) { // 如果为0 说明是新增元素
    patch(
    null,
    nextChild,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG
    )
} else if (moved) {
    if (j < 0 || i !== increasingNewIndexSequence[j]) {
        // 需要移动
        move(nextChild, container, anchor, MoveType.REORDER)
    } else {
        j-- // 元素不需要移动
    }
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

根据之前计算出来的数组,来确定最长增长稳定序列(不需要做移动的节点),循环新节点如果值为0则说明需要新增元素,否则查看节点是否需要移动