Vue3在模板编译和diff算法方面作了许多优化,最近我就了解到了静态节点作用域提升(hoist)以及利用BLockTree来优化diff算法。下面我来分别大概介绍这两种优化的具体实现。
了解到的知识来自于《vuejs设计与实现》
静态节点作用域提升
我们知道,在挂载时,会调用组件实例上的render函数来获取组件对应的vnode。在更新时也会重新调用render函数来获取到新的vnode,然后对新旧vnode进行patch(diff算法)操作。
假设我们有下面这个组件
转化后的render函数如下,可以看到,静态节点<div>jzsp</div>的vnode作为一个全局变量,被提升到了render函数外,在render函数内部返回这个vnode对象的引用。
这样做的好处是,在我们修改响应式数据,导致重新渲染而执行render函数时,不需要重复的创建这些静态节点(因为他们是不会改变的),省去了静态节点创建vnode的过程,优化了模板编译的性能。
patchFlag、Block优化diff算法
传统diff的缺点
传统的diff算法,会对vnode中所有的children分别进行判断,找出新旧vnode的共同之处,尽可能的复用结点而避免创建dom造成的性能开销。
但是这有一个坏处就是,传统的diff算法会对静态节点也进行处理,但是这些静态节点是不会变化的,对静态节点进行diff比较是没有意义的。
vue3的改进:patchFlag
在Vue2中是无法在diff算法执行过程中过滤静态节点的,因为vnode中保存的信息太少,无法判断一个结点是静态的还是动态的。所以Vue3在进行diff优化时,在vnode上添加了额外的信息,使得在运行时能区分这个vnode是动态的还是静态的。
假如我们有下面这个模板
<div>
<div>jzsp</div>
<p>{{ message }}</p>
</div>
传统的虚拟DOM会这样描述上面的模板:
const vnode = {
tag:'div',
children:[
{ tag:'div',children:'jzsp' },
{ tag:'p',children:ctx.message }
]
}
传统的虚拟DOm中没有任何的标志能识别出某个结点是静态的还是动态的,所以在Vue3优化之后,新的虚拟DOM会这样描述上面的模板:
const vnode = {
tag:'div',
children:[
{ tag:'div',children:'jzsp' },
{ tag:'p',children:ctx.message , patchFlag:1 } //这是动态结点
]
}
也就是说,如果是动态结点的话,对应的vnode中会存在patchFlag属性。
vue3的改进:Block
既然我们已经可以判断vnode是否是动态结点了,那么我们就可以在虚拟结点创建的时候,将它子代的动态虚拟结点提取出来,并且保存在该虚拟节点的dynamicChildren数组内。
const vnode = {
tag:'div',
children:[
{ tag:'div',children:'jzsp' },
{ tag:'p',children:ctx.message , patchFlag:1 } //这是动态结点
],
dynamicChildren:[
// p标签具有patchFlag属性,因此它是动态结点。
{ tag:'p',children:ctx.message , patchFlag:1 }
]
}
观察上面的vnode对象可以发现,与普通的虚拟结点比,它多出了一个额外的属性:dynamicChildren。我们把带有这个属性的虚拟节点称之为“块”,也就是Block。
渲染器在更新一个Block时,会直接取出dynamicChildren进行更新,进而忽略掉那些静态节点。
注意:并不是所有的vnode都是Block,当我们在编写模板代码的时候,模板的根节点会是一个Block结点。
<template>
<!-- 这个div是Block -->
<div>
<p>jzsp</p>
</div>
</template>
Block收集动态结点
我们得先知道一点:在render函数内,对createVNode函数的调用是层层嵌套的结构,并且函数的执行顺序是从内而外的(因为如果要执行最外层的函数的话,必须得先执行完第二层的函数并获取返回值,以此类推)。如下:
// 最内层的createVnode先执行完成。
createBlock('div',{},[
createVnode('div',{},[
createVnode('div',{},[
createVnode('div',{},[
])
])
])
])
让我们来重新看一下前面贴出来的转化后的render函数。可以看到在return的时候,先后调用了openBlock和createBlock方法,并且createBlock内部也是嵌套地调用了createVnode方法
先来介绍openBlock,它会创建一个数组(用于存储VNode)并且压入全局的blockStack(存储的是VNode数组)中,同时将这个创建的数组保存在全局的变量currentBlock中。
接着让我们看一下createVnode函数跟BLock有关的核心实现。在createVode最后会判断currentBlock是否存在,以及判断patchFlag(判断是否是动态结点),如果是动态结点并且currentBlock存在的话,就将当前的vnode压入全局的currentBlock中。具体如下:
再让我们看一下最后执行的createBlock函数,它做的事情很简单,就是将全局的currentBLock赋值到当前vnode对象的dynamicChildren属性中(此时的currentBlock已经包含了子代的所有动态结点,因为嵌套层级时内部先执行),如下。
那么在我们更新组件的时候,会优先判断新旧vnode中是否存在dynamicChildren属性。如果有的话,就只对这些dynamicChildren进行diff算法的比较。这样就可以省去大量的静态节点的比较,提高了vue的性能。