前言
本篇文章是对上一篇文章的补充,对一些遗漏和其他内容进行分析补充。
Fargment类型对比
文档碎片是一堆没有父节点的元素,更新执行的是processFragment
函数中的一部分(位置在runtime-core/src/renderer.ts
)
稳定片段不需要对比子节点顺序,但是它可能会有dynmiacChildren
,需要进行对比,patchBlockChildren
具体流程可以去看我上一篇文章。对比完成之后,
但是,如果在稳定的片段上有一个key,说明它是一个<template v-for>
,确保所有的根级别的VNode
继承el
,他会去递归寻找定位旧的el
,以便在更新节点时引用,避免造成el is null
,虽然在更新的时候就已经继承了,但是需要确保所有都继承。
键控/非键控它们都是经过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
,那么我们先看用index
做key
值来追踪他是如何更新。
我们只需要关心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
的值却完全不同了,按照正常的情况下,第一个新节点完全可以复用旧的最后一个节点。
但是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]
,结合这个用例进行分析其流程。
方法名字是getSequence
,他接受一个数组,最后求的是最长递增子序列中的值在原数组中的索引,开头会复制一份原数组赋值给p
,用做映射,result
是用例存放结果索引的,默认给一项0len
是数组长度,而i、j、u
等等这些变量在第一部分是用不到的,在后面会使用。
首先会遍历原数组,按顺序取出,赋值给arrI
,这里j
会被赋值成result
的最后一个,也就是当前已经求出来的子序列中的最后一个,arrI
和arr[j]
比较,只有当arrI
比arr[j]
大,说arrI
比当前子序列中最大的还要大,会把当前项的索引push
到result
中,并且在p
中添加映射。就可以跳过该层循环,处理下一项(PS:数组p
中会记录着每一项在最长递增子序列中的前一个是第几项)。
在用例中,前两项可以通过,result
变成[0, 1]
,接下来的极限都不能通过,执行下面的流程。
如果不是大于的情况,需要继续往下走,这里使用的是二分查找,u
是result
的第一个位置,v
是result
的最后一个位置。这里的二分查找用的是位运算,也就是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]
,而后面第五项比现在的子序列的最后一个大,直接push
到result
中,并在p
中添加索引。
而剩下的虽然都和result
的最后一项一样,但是如果在二分查找之后找不到理想位置,就不会添加到子序列中。
经过前面的流程,产生了最长递增子序列的雏形,但是可能不正确,我们还需要从后往前推,得出正确的最长递增子序列,结合p
中的映射,便可以一个一个得出。
这个时候,p
的值是[7, 0, 3, 2, 3, 4, 9, 9, 9, 9]
,result
这里不需要知道,后面会重新赋值,在用例中,v
是result
的最后一项,也就是最长递增子序列中最大的一项,u
是result
的长度,在用例中,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中的最长递增子序列算法。
好了,到了文章的最后,还是希望各位哥哥姐姐能指导指导。有说错或者遗漏的欢迎在评论区讲解,谢谢。