第十节:vue3 Diff 算法 顺序比对

241 阅读2分钟

入口 patchKeyedChildren

就是第九节中 当新旧子节点都是数组时进行diff算法

// 有key的情况下 比较两个子节点的差异
const patchKeyedChildren = (c1, c2, el) => {
    let i = 0;

    // 拿到子集的长度
    let e1 = c1.length - 1;
    let e2 = c2.length - 1;

    //...
}
const patchChildren = (n1, n2, el) => {
    const c1 = n1 && n1.children
    const c2 = n2 && n2.children

    const prevShapeFlag = n1.shapeFlag;
    const shapeFlag = n2.shapeFlag;
    if(shapeFlag & ShapeFlags.TEXT_CHILDREN){ // 1、新节点是 文本
       //...
    }else {
        if(prevShapeFlag & ShapeFlags.ARRAY_CHILDREN){  // 2、新的 是数组
            if(shapeFlag & ShapeFlags.ARRAY_CHILDREN){  // 数组 数组
                // diff算法
                patchKeyedChildren(c1, c2, el); // 全量更新 全量比对
            }else{
                //...
            }
        }else{ // 3、新的既不是文本 也不是 数组,就剩下一种情况为空
           // ...
        }
    }
}

diff算法的整体流程

  1. 从头比对一遍 (动索引)
  2. 从尾比对一遍 (动子集的长度)
  3. 同序列加挂载 i>e1 && i<e2
  4. 同序列加卸载 i>e2 && i<=e1
  5. 乱序比对
  • 5.1、找到 新的vnode需要比对区域
  • 5.2、遍历老的vnode的需要比对区域
  • 5.3、移动位置 倒序插入

实现

有key的情况下 比较两个子节点的差异

1. sync from start: i <= e1 && i <= e2 有任何一方停止循环则立即跳出 (主要是找i)

移动索引下标i

找到从起始位置 的最大的复用元素索引(i)(0到i就是 从起始位置开始到有变化位置不用动元素)

image.png

// 1. sync from start: i <= e1 && i <= e2
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {  // 有任何一方停止循环则立即跳出
    const n1 = c1[i];
    const n2 = c2[i];
    if (isSameVNodeType(n1, n2)) {  // 是同一个
        patch(n1, n2, el) // 这样做就是递归比较两个节点的属性和子节点
    } else {
        break;
    }
    // 移动的是下标指针 i
    i++;
}
// console.log(i, e1, e2) // 2 2 3  尽可能减少比较的内容

2. sync from end: i <= e1 && i <= e2 主要是找(e1,e2)

移动的是新老子节点的长度

找到从结尾位置 开始的 最大的复用元素索引(e1,e2)(从结尾位置开始到有变化位置,就是不用动的元素)

image.png

// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
    const n1 = c1[e1];
    const n2 = c2[e2];
    if (isSameVNodeType(n1, n2)) {
        patch(n1, n2, el);
    } else {
        break;
    }
    // 移动的是 两个目标的长度
    e1--;
    e2--;
}
console.log(i, e1, e2) //  0 0 1 ,i一直是经过从头比对的索引  e1比较短走到了0 e2差一位为1

查找示例: image.png

下边的 3、4步有一方比较完毕了 要么删除 要么添加

3. common sequence + mount 同序列加挂载 i>e1 && i<=e2 说明有新增

上边两种循环 都是

i 要比e1大说明有新增的部分

i停留的值 和 e2之间的 为新增的

image.png

image.png

// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
if (i > e1) { // 说明有新增 
    if (i <= e2) { // 表示有新增的部分
        // 先根据e2(因为e2是新的vnode) 取他的下一个元素索引extPos  与c2的长度比较看是否超出 超出的话直接push 不超出的话要插入到c2[nextPos].el 之前
        const nextPos = e2 + 1;
        // extPos >= c2.length 说明直接push到容器后边即可
        // extPos < c2.length 说明需要插入到指定元素前边
        const anchor = nextPos < c2.length ? c2[nextPos].el : null;
        while (i <= e2) { 
            // 创建新节点扔到容器中  anchor是insert用的 层层传递; 
            // anchor为null 说明直接push到尾部即可
            patch(null, c2[i], el, anchor); 
            i++;
        }
    }
}

4. common sequence + unmount 同序列加卸载 i>e2 && i<=e1

i比e2大说明有要卸载的

i到e1之间的就是要卸载的

image.png

image.png

// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1  删除第2个
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1 删除第0个
else if (i > e2) {
    while (i <= e1) {
        unmount(c1[i]) // 删掉老的
        i++
    }
}

完整版

// 有key的情况下 比较两个子节点的差异
const patchKeyedChildren = (c1, c2, el) => {
    let i = 0;

    // 拿到子集的长度
    let e1 = c1.length - 1;
    let e2 = c2.length - 1;

    // 1. sync from start
    // (a b) c
    // (a b) d e
    while (i <= e1 && i <= e2) {  // 有任何一方停止循环则立即跳出
        const n1 = c1[i];
        const n2 = c2[i];
        if (isSameVNodeType(n1, n2)) {  // 是同一个
            patch(n1, n2, el) // 这样做就是递归比较两个节点的属性和子节点
        } else {
            break;
        }
        // 移动的是下标指针 i
        i++;
    }
    // console.log(i, e1, e2) // 2 2 3  尽可能减少比较的内容

    // 2. sync from end
    // a (b c)
    // d e (b c)
    while (i <= e1 && i <= e2) {
        const n1 = c1[e1];
        const n2 = c2[e2];
        if (isSameVNodeType(n1, n2)) {
            patch(n1, n2, el);
        } else {
            break;
        }
        // 移动的是 两个目标的长度
        e1--;
        e2--;
    }
    console.log(i, e1, e2) //  0 0 1 ,i一直是经过从头比对的索引  e1比较短走到了0 e2差一位为1


    // 下边的 3、4步有一方比较完毕了 要么删除 要么添加

    // 3. common sequence + mount 同序列加挂载  i>e1 && i<e2
    // 上边两种循环 都是
    // i 要比e1大说明有新增的部分
    // i停留的值 和 e2之间的 为新增的 
    // (a b)
    // (a b) c
    // i = 2, e1 = 1, e2 = 2
    // (a b)
    // c (a b)
    // i = 0, e1 = -1, e2 = 0
    if (i > e1) { // 说明有新增 
        if (i <= e2) { // 表示有新增的部分
            // 先根据e2 取他的下一个元素  看是否存在 存在的话往前插 不存在的话往后插
            const nextPos = e2 + 1;
            const anchor = nextPos < c2.length ? c2[nextPos].el : null;
            while (i <= e2) { 
                patch(null, c2[i], el, anchor); // 创建新节点扔到容器中  anchor是insert用的 层层传递
                i++;
            }
        }
    }

    // 4. common sequence + unmount 同序列加卸载 i>e2 && i<=e1
    // i比e2大说明有要卸载的
    // i到e1之间的就是要卸载的
    // (a b) c
    // (a b)
    // i = 2, e1 = 2, e2 = 1
    // a (b c)
    // (b c)
    // i = 0, e1 = 0, e2 = -1
    else if (i > e2) {
        while (i <= e1) {
            unmount(c1[i]) // 删掉老的
            i++
        }
    }

    // 乱序比对
}

下一章我们来实现乱序比对