Vue3.2 diff算法分析补充

1,626 阅读6分钟

f5dd25c2527841ec883df8f3ceadc983.jpg

前言

本篇文章是对上一篇文章的补充,对一些遗漏和其他内容进行分析补充。

Fargment类型对比

文档碎片是一堆没有父节点的元素,更新执行的是processFragment函数中的一部分(位置在runtime-core/src/renderer.ts)

image.png

稳定片段不需要对比子节点顺序,但是它可能会有dynmiacChildren,需要进行对比,patchBlockChildren具体流程可以去看我上一篇文章。对比完成之后,

但是,如果在稳定的片段上有一个key,说明它是一个<template v-for>,确保所有的根级别的VNode继承el,他会去递归寻找定位旧的el,以便在更新节点时引用,避免造成el is null,虽然在更新的时候就已经继承了,但是需要确保所有都继承。

image.png

键控/非键控它们都是经过v-for编译后产生的片段,所以它们每一个子项都是一个块,它们一定没有dynamicChildren,执行patchChildren,具体流程分析在上一篇文章中。

为什么不推荐使用index作为key

首先明确一点,这里的vue版本是vue3.2。

节点反转案例

假设我们有如下代码:

const {createApp, reactive, defineComponent} = Vue

const Comp = defineComponent({
    template: `
        <li>{{num}}</li>
    `,
    props: ['num']
})

  const App = defineComponent({
    template: `
<ul>
  <Item v-for="(item, index) in list" :key="index" :num="item"></Item>
</ul>
<button @click="changeList">changeList</button>
    `,

    setup() {
        let list = reactive([1,2,3,4,5,6])

        function changeList() {
            list = list.reverse()
        }

        return {
            list,
            changeList
        }
    },
    components: {
        Item
    }
  })

  const app = createApp(App)
  app.mount('#app')

这其实是很简单的列表渲染而已,使用Comp组件渲染出list中的每一项,并且将传递了num作为props,那么我们先看用indexkey值来追踪他是如何更新。

我们只需要关心Item列表组件的更新,在初次渲染的时候,vDOM列表oldChildren大概的表示如下:

[
    {
        type: {/* 组件配置 */},
        key: 1,
        props: {
            key: 0,
            num: 1
        }
    },
    {
        type: {/* 组件配置 */},
        key: 1,
        props: {
            key: 1,
            num: 2
        }
    },
    {
        type: {/* 组件配置 */},
        key: 2,
        props: {
            key: 2,
            num: 3
        }
    },
    ......
]

当点击按钮是,触发changeList函数,会对数组进行反转操作,这个时候产生的新的vDOM列表newChildren大概表示如下:

[
    {
        type: {/* 组件配置 */},
        key: 0,
        props: {
            key: 0,
            num: 5
        }
    },
    {
        type: {/* 组件配置 */},
        key: 1,
        props: {
            key: 1,
            num: 4
        }
    },
    {
        type: {/* 组件配置 */},
        key: 2,
        props: {
            key: 2,
            num: 3
        }
    },
    ...
]

这个时候可以发现,虽然key的值没有发生变化,但是传递的num的值却完全不同了,按照正常的情况下,第一个新节点完全可以复用旧的最后一个节点。

image.png

但是vue中判断两个节点是不是同一个节点的方法isSameeVNodeType中除了判断type是不是相同节点,最重要的是判断key值对比,而在patchKeyedChildren中是旧首节点和新首节点对比,在上述的例子中,新旧节点对比,因为key相同,所以被认为是相同的节点,但其实是错误的相同节点,可能会全部更新,也有可能不更新

但是如果用唯一值(这里先用item代替),进行反转之后,这个时候产生的新的vDOM列表newChildren大概表示如下:

[
    {
        type: {/* 组件配置 */},
        key: 5,
        props: {
            key: 5,
            num: 5
        }
    },
    {
        type: {/* 组件配置 */},
        key: 4,
        props: {
            key: 4,
            num: 4
        }
    },
    {
        type: {/* 组件配置 */},
        key: 3,
        props: {
            key: 3,
            num: 3
        }
    },
    ...
]

在这种情况下,patchKeyedChildren中会直接去到流程五,只会复制属性和移动节点。

最长递增子序列

patchKeyedChildren中的流程五,有用到一个算法:"最长递增子序列",这是求出最长递增子序列的结果,而不是最长递增子序列的长度。(代码位置在:runtime-core/src/renderer.ts),

假设我们现在有一个数组:[7, 8, 3, 4, 5, 9, 9, 9, 9],结合这个用例进行分析其流程。

image.png

方法名字是getSequence,他接受一个数组,最后求的是最长递增子序列中的值在原数组中的索引,开头会复制一份原数组赋值给p,用做映射,result是用例存放结果索引的,默认给一项0len是数组长度,而i、j、u等等这些变量在第一部分是用不到的,在后面会使用。

首先会遍历原数组,按顺序取出,赋值给arrI,这里j会被赋值成result的最后一个,也就是当前已经求出来的子序列中的最后一个,arrIarr[j]比较,只有当arrIarr[j]大,说arrI比当前子序列中最大的还要大,会把当前项的索引pushresult中,并且在p中添加映射。就可以跳过该层循环,处理下一项(PS:数组p中会记录着每一项在最长递增子序列中的前一个是第几项)。

在用例中,前两项可以通过,result变成[0, 1],接下来的极限都不能通过,执行下面的流程。

image.png

如果不是大于的情况,需要继续往下走,这里使用的是二分查找,uresult的第一个位置,vresult的最后一个位置。这里的二分查找用的是位运算,也就是c = (u + v) >> 1,得出了result中间位置c,二分查找是在result中查找到当前递增子序列中的值对应arr的值与当前对比项尽量相差最小的值(不一定是按顺序的),也就是arr[result[u]],只有arrI大于这个值,才会修改result[u]为当前项的索引,并在u大于0的情况下去修改p的映射,u等于0说明这一项是子序列的第一个,也就不需要映射前一个。

在用了中,第三项和第四项以及第五项都比result的最后一项大,只能通过二分查找去找和自己相差最小并将其在result中替换,第三项是第一个不添加映射,第四项不是第一个会添加映射,result变成[2, 3, 4],而后面第五项比现在的子序列的最后一个大,直接pushresult中,并在p中添加索引。

而剩下的虽然都和result的最后一项一样,但是如果在二分查找之后找不到理想位置,就不会添加到子序列中。

image.png

经过前面的流程,产生了最长递增子序列的雏形,但是可能不正确,我们还需要从后往前推,得出正确的最长递增子序列,结合p中的映射,便可以一个一个得出。

这个时候,p的值是[7, 0, 3, 2, 3, 4, 9, 9, 9, 9]result这里不需要知道,后面会重新赋值,在用例中,vresult的最后一项,也就是最长递增子序列中最大的一项,uresult的长度,在用例中,u是4,v是5,进入循环u--u变成了3,v赋值给result[3],然后将p[5]赋值给v,如此循环下去,便可得出[2, 3, 4, 5]是最长递增子序列。

总结

本篇文章是上一篇文章diff算法的补充。补充了Fragment的对比和更新。顺带分析了为什么不要使用index key,因为在对比中判断是不是相同节点的依据是key,使用index key可能会导致因为key虽然相同而找到错误的相同节点,这会导致完全更新或者干脆不更新。还分析了vue中的最长递增子序列算法。

好了,到了文章的最后,还是希望各位哥哥姐姐能指导指导。有说错或者遗漏的欢迎在评论区讲解,谢谢。