第十一节:vue3 Diff 算法 乱序比对

277 阅读2分钟

入口:

接上一章的 patchKeyedChildren 函数

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

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

    // 1. sync from start
    // ...

    // 2. sync from end
    // ...
    
    // 3. common sequence + mount 同序列加挂载  i>e1 && i<e2
    // ...

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

    5.乱序比对unknown sequence
    // ...
}

乱序比对

c1为老的子节点 c2为新的子节点

s1为老的子节点的第一个比对区的下标索引 s2为新的子节点的第一个比对区的下标索引 , 都是上一节从前向后比对的i

e1为老的子节点的最后一个比对区的下标索引 e2为新的子节点的最后一个比对区的下标索引

1、找到 新的vnode需要比对区域

后面叫:新增比对区,就是newVnode[i]newVnode[e2]

建立key和索引的映射表(keyToNewIndexMap): key -> newIndex

image.png

// 乱序比对
render( 
    h(
        'div', 
        { style: { 'color': 'red' } },
        [
            h('li', { key: 'a' }, 'a'),
            h('li', { key: 'b' }, 'b'),
            h('li', { key: 'c' }, 'c'),
            h('li', { key: 'd' }, 'd'),
            h('li', { key: 'e' }, 'e'),
            h('li', { key: 'f' }, 'f'),
            h('li', { key: 'g' }, 'g'),
        ]
    ), 
    app
)
setTimeout(() => {
    render(
        h(
            'div',
            { style: { 'color': 'blue', background: 'red' } },
            [
                h('li', { key: 'a' }, 'a'),
                h('li', { key: 'b' }, 'b'),
                h('li', { key: 'e' }, 'e'),
                h('li', { key: 'c' }, 'c'),
                h('li', { key: 'd' }, 'd'),
                h('li', { key: 'h' }, 'h'),
                h('li', { key: 'f' }, 'f'),
                h('li', { key: 'g' }, 'g'),
            ]
        ), 
        app
    )
},1000)
// a b [c d e] f g
// a b [e c d h] f g
// i = 2, e1 = 4, e2 = 5
const s1 = i;
const s2 = i;

// key和i的映射表
const keyToNewIndexMap = new Map();
// 循环 新增比对区 每一项 建立key和索引的映射 因为老的子集就算再多也没用
for (let i = s2; i <= e2; i++) {
    const nextChild = c2[i];
    keyToNewIndexMap.set(nextChild.key, i);
}
console.log(keyToNewIndexMap,'keyToNewIndexMap')
打印结果:

image.png

2、遍历老的vnode的需要比对区域

后面叫 老的比对区

看一下新的里边有没有,有的话要比较差异,没有的话要添加到列表中

老的有新的没有要删除

只是比对新老属性和子集 没有移动位置

// 新增比对区的总个数: e2 - s2 + 1 
// +1是因为 在数组中 索引减去索引 会比真实元素少一个
const toBePatched = e2 - s2 + 1;
// 新增比对区 和 老的比对区 的 索引映射表;
// 以toBePatched为长度的数组 默认每一项全部是 0
// 如果数组里放的值大于0 说明path过了 不是新增的; 等于0是新增的
const newIndexToOldMapIndex = new Array(toBePatched).fill(0); 
for (let i = s1; i <= e1; i++) {
    const oldChild = c1[i];  // 老的比对区的children
    
    // 用老的比对区的key 在 新增比对区索引映射表keyToNewIndexMap中 获取新的里边有没有对应的索引
    let newIndex = keyToNewIndexMap.get(oldChild.key); 
    if (newIndex == undefined) {// 1、老的有&新的没有 直接删除
        unmount(oldChild); 
    } else { // 2、老的有新的有 比对差异
        // 需要比对的都是 老的新的都有的key 记录新的索引对应的老的索引
        // 如果数组里放的值大于0 说明path过了  (i + 1 为了让i=0和 默认的0 区分开)
        // newIndex - s2 是因为从第s2个开始比对的
        newIndexToOldMapIndex[newIndex - s2] = i + 1; 
        patch(oldChild, c2[newIndex], el);
    }
}
console.log(newIndexToOldMapIndex, 'newIndexToOldMapIndex')
// [5, 3, 4, 0] 是0的 说明就是新增的
打印结果

35B088E4A75359BA77F5B234D35FD535.png

3、移动位置 倒序插入

image.png

// toBePatched - 1 是 新增比对区的 "第一项索引 到 最后一项索引" 差的步数
for (let i = toBePatched - 1; i >= 0; i--) {
    const nextIndex = s2 + i; // [e,c,d,h]   找到最后一个h的索引 
    const nextChild = c2[nextIndex]; // 找到 h
    // 找到当前元素h的 下一个已比对的元素; 
    // nextIndex + 1 >= c2.length说明是 新的vnode的最后一项 直接push到容器中
    // nextIndex + 1 < c2.length 说明要插入到下一个已比对的元素前边
    let anchor = nextIndex + 1 < c2.length ? c2[nextIndex + 1].el : null;

    // 当前的h是新增的没有el 
    // 需要看一下 当前的有没有 如果没有需要新增
    // newIndexToOldMapIndex[i] 是0的 说明这是一个新元素 直接创建插入到 容器最后即可
    if (newIndexToOldMapIndex[i] == 0) { 
        patch(null, nextChild, el, anchor)
    } else {
        // 插入: 根据参照物 将节点直接移动过去  所有节点都要移动 (但是有些节点可以不动)
        hostInsert(nextChild.el, el, anchor);
    }
}
// 目前对所有的新增节点都进行了一遍倒叙插入,其实类根据刚刚的数组来减少插入 (newIndexToOldMapIndex:[5, 3, 4, 0])

// 最长递增子序列

下一章我们来实现最长递增子序列