一、简要介绍
在Vue3整个框架中,主要分为runtime以及complier两个部分。
- complier 负责编译解析
- runtime 提供运行时环境
两部分有着极为紧密的前后关联关系。complier解析数据并提供有效信息给runtime,促使runtime能更高效的运行。
二、传统的diff算法
diff算法直译过来就叫做差异更新算法。Vue2和Vue3在差异更新上存在着很大的不同,Vue2的diff算法在下文统称为传统的diff算法,下面简要介绍这种diff算法的思想。
1、算法逻辑
传统的diff算法的核心逻辑就是全量比较。假如存在如下模板:
<div>
<span>测试</span>
<i>斜体</i>
<span>{{ data }}</span>
</div>
当data数据变更后,会触发模板重新渲染,此时进行差异更新,更新步骤如下:
这种模式的表现就是从dom树的根节点开始比较,如果某个元素变更,则更新这个元素,直到完成整个比较流程。
2、存在的问题
虽然传统的diff算法能够准确的完成更新操作,但其中有着许多不需要的比较操作。比如上图中的1,2,3步骤就是无效操作,因为这些元素都是静态的,不可能更改的。
当一个模板静态内容比较多,使用这种全量的比较更新,就会大大的浪费的性能,因此Vue3使用了全新的diff算法,下文统称为优化的diff算法
三、虚拟节点
diff算法的目的是进行比较,然后进行差异更新。在实际过程中,是没有直接使用dom元素进行比较的,而是有一棵虚拟节点树,通过比较树上的节点,最终映射到dom树上。
因此在聊优化的diff算法前,先说说虚拟节点中的几个关键节点。
1、虚拟节点的作用
虚拟节点树本质上是dom树的映射。每一个dom元素,都可以找到与之对应的虚拟节点,但虚拟节点树是要比dom树更大的,因为有些虚拟节点并没有被转换为dom元素,它们是一种“功能节点”。
2、Block节点与Block Tree
在了解虚拟节点与dom元素之间的关系之后,我们来接下来谈谈虚拟节点的另外一重身份,Block节点。一个虚拟节点,可能是一个Block节点。它们的区别如下:
-
普通虚拟节点:
const node:VNode = { type:'div', children:[ { type:'span', children:'测试'}, { type:'i',children:'斜体'}, { type:'span',children:ctx.data} ] }
-
Block节点:
const node:VNode = { type:'div', children:[ { type:'span', children:'测试'}, { type:'i',children:'斜体'}, { type:'span',children:ctx.data, patchFlag:1 /**动态的文本**/} ], dynamicChildren:[ { type:'span',children:ctx.data,patchFlag:1} ] }
可以看出,Block节点比一个普通虚拟节点多了一个关键属性dynamicChildren,这个属性用于存放所有的动态节点。传统的diff算法会进行全量比较,那么怎么去进行优化呢?最理想的方式就是把会变化的节点收集起来,每次只比较这部分节点即可,这种方式直接过滤掉了静态节点,极为高效。因此在执行diff算法时,就可以直接比较dynamicChildren里面的节点,这样极大的减少了比较的次数。
当然,dynamicChildren节点里面不仅要存放动态节点,还要存放Block节点,这样就可以构成如下结构:
- Block(div)
- VNode(span)
- Block(div)
- VNode(i)
- VNode(span)
- VNode(span)
这样就可以构成一棵Block Tree,通过比较Block Tree进行差异更新,就是优化的diff算法的核心思想,只比较动态节点,跳过静态节点。
3、Fragment节点
Block节点的介绍告一段落,现在介绍另一个特殊节点,Fragment节点。Fragment节点是虚拟节点的一种,它是一种功能节点,不会生成任何dom元素。这个节点是一组虚拟节点的逻辑父级,当渲染这个节点时,会直接渲染其子节点。这个表述可能不大容易理解,我用下面的例子来阐述其作用。
-
实现多根节点
假如一个组件存在如下模板:
<template> <div></div> <div></div> </template>
在Vue2中,这种模板是无法通过编译的。Vue2解析这种模板会生成一棵多根树,但Vue2只支持单根节点。但在Vue3中,这种模板是支持的,这并不是说Vue3支持多根节点,而是Vue3会把这种多根模板转换为单根节点的虚拟节点树,其根节点就是Fragment节点。结构如下:
- VNode(Fragment) - VNode(div) - VNode(div)
-
维持Block Tree的稳定树结构
假如一个组件存在如下模板:
<template> <div> <i>{{ item.length }}</i> <div v-for="item in items"> <span>{{item.name}}</span> </div> <div>{{ name }}</div> </div> </template>
通过前面的讲解,这个模板最终会解析为如下Block Tree:
- Block(div) - VNode(i) - VNode(span) ... // 动态节点span的数量根据items的大小确定 - VNode(span) - VNode(div)
假如items的数据数量变化,那么变更后的Block Tree结构和之前的Block Tree结构就无法匹配,但优化的diff算法必须保证Block Tree变更前后保持一致的树结构,只有这样才能进行快速的节点比较。因为Block节点收集动态节点是跨越层级的(比如上例中v-for指令所在的div节点没被收集,其子节点span节点被收集),当树的层级结构不一致时,无法确定删减或新增的节点。
那么转换后的Block Tree如下:
- Block(div) - VNode(i) - Block(Fragment) //它是Block节点,不管items的大小是怎样的,这儿都是1个Fragment节点 - VNode(span) ... //根据items的大小确定 - VNode(span) - VNode(div)
4、生成Block节点的情况
在上文已经说过,通过比较Block Tree就是实现优化的diff算法的核心思想,那么接下来就聊聊什么情况下会生成Block节点。
本质上所有能导致虚拟节点树不稳定的地方,都需要生成Block节点,外加组件根节点必须是Block节点来作为Block Tree的根节点。主要如下:
-
根节点
根节点必须是一个Block节点,因为一棵Block Tree必须得有一个节点作为初始节点,这个初始节点就是根节点。
-
Fragment节点
前文说过,Fragment节点是功能节点,不渲染任何实体的dom元素。它就像是一个节点容器一样,将一组不稳定渲染的节点封装为一个容器节点,以便于维持Block Tree的稳定性。
但稳定也是相对的,我还是用上面介绍Fragment节点时的例子来说明,针对以下模板:
<template> <div> <i>{{ item.length }}</i> <div v-for="item in items"> <span>{{item.name}}</span> </div> <div>{{ name }}</div> </div> </template>
如果没有Fragment,那么Block Tree将不稳定,因此无法快速比对节点。其实即便有了Fragment节点,Block Tree在结构上也不是完全稳定的,示例Block Tree如下:
- Block(div) - VNode(i) - Block(Fragment) - VNode(span) ... //根据items的大小确定 - VNode(span) - VNode(div)
Fragment节点的子节点数量不固定,因此Fragment节点内部是不稳定的。但对于整棵Block Tree而言,只要把Fragment节点视为一个节点而不考虑其内部变化,那么Block Tree就是稳定的。
其实Fragment节点不全是不稳定的,也存在稳定的Fragment节点,比如以下模板:
<template> <div> <div v-for="num in 5"> <span>{{ items[num].name }}</span> </div> </div> </template>
这种v-for指令生成的Fragment节点永远都只有5个子节点,因此便称之为稳定的Fragment节点。
稳定的Fragment节点和不稳定的Fragment节点在执行diff算法时存在很大的区别。当比对2个Fragment节点时,本质是比较其子节点。
-
稳定的Fragment
稳定的Fragment节点有着稳定个数的子节点,可以采用优化的diff算法来快速比对。
-
不稳定的Fragment
不稳定的Fragment节点有着不稳定个数的子节点,只能采用传统的diff算法来比对。
-
-
v-if v-else v-else-if
条件指令会直接导致某个节点不被生成,这会破坏Block Tree的稳定性,因此也会生成Block节点,针对如下模板:
<template> <div> <span v-if="show"></span> </div> </template>
当满足条件时很好理解,Block Tree如下:
- Block(div) - Block(span)
当不满足条件会生成一个注释节点用于保证Block Tree的稳定性,注释节点不会渲染任何dom元素,就是一个功能性节点,如下:
- Block(div) - Block(comment)
其实条件指令不仅仅是单分支渲染会破坏Block Tree的稳定性。在多分支渲染时,每个分支的结构也可能不一样,这也会破坏Block Tree的结构稳定性。因此vue-sfc模块在编译条件指令时,都会给这些节点添加一个key用于保证不同分支节点是唯一的。key是节点唯一标识,一旦key不一样,那么就会认为是不一样的节点。
简要解析代码如下:
-
动态key绑定的节点
上面提到,key是节点的唯一标识。即便节点的所有属性的一致,但其key不一样,那么这也是不一样的节点。当动态key绑定的节点不作为一个Block节点时,那么其子节点中的动态节点就会被上层Block节点收集,这是不对的。因为一旦key变化,这个节点及其子节点就会被卸载然后重建,一旦被上层收集就会引发异常。
其实动态key绑定的节点和条件指令的原理很相似,条件指令本身也有利用key来保证分支的唯一性。
四、PatchFlags
前文主要都是针对Block Tree在进行描述。Block Tree的目的是减少比较VNode的次数,尽量只比较动态节点。但每个节点都很有多属性,如果能够快速定位每个节点变更的属性,那么针对渲染优化而言,也有很大帮助。
在Vue3中,complier模块在编译模板时,会分析模板中所有的动态属性,并且对这些动态节点赋予一个PatchFlag,当在进行更新时,只需要去判断PatchFlag,便能快速的进行更新操作。
下面是针对PatchFlag的简单总结:
export const enum PatchFlags {
// 动态的文本,只需要比较文本内容
TEXT = 1,
// 动态的class,只需要比较class
CLASS = 1 << 1,
// 动态的样式,只需要比较样式
STYLE = 1 << 2,
// 动态的属性值,只需要比较指定属性 :name="name"
PROPS = 1 << 3,
// 动态的属性,比较全部属性 :[prop]="prop"
FULL_PROPS = 1 << 4,
// 自定义的事件
HYDRATE_EVENTS = 1 << 5,
// 稳定的Fragmengt
STABLE_FRAGMENT = 1 << 6,
// 有key的Fragment
KEYED_FRAGMENT = 1 << 7,
// 没有key的Fragment
UNKEYED_FRAGMENT = 1 << 8,
// 没有属性变化的更新 比如ref,每一次更新完成后都需要设置ref,ref可能因为其他值的影响而变化
NEED_PATCH = 1 << 9,
// 动态的插槽
DYNAMIC_SLOTS = 1 << 10,
// 开发模式使用 不考虑
DEV_ROOT_FRAGMENT = 1 << 11,
// 静态提升的节点
HOISTED = -1,
// 节点必须走传统diff,不能使用优化模式
BAIL = -2
}
一旦给某个节点打上了PatchFlag,那么在更新时,便可以根据PatchFlag去快速的更新,而不用所有的属性一一比较。
五、静态提升
静态提升很好理解,在执行render函数时,会动态的生成VNode,但许多节点是静态节点,在更新时没有必要再次创建,因此将这部分节点保存起来二次复用,就是静态提升。简单示意如下:
静态提升功能在遇到大批量静态模板存在时,还会将其自动预字符串化。
当遇到大量连续静态模板时,会将这些静态模板拼接成html,这样会大大减少生成的VNode数量。
六、事件缓存
当在开发时,我们常常不经意间会写出内联的事件,比如:
<div @click="() => {}"></div>
每当更新重新执行render函数时,这个内联的匿名函数都会被执行,这样会造成内存的浪费。在Vue3中,会将这种内联事件进行缓存,避免二次创建。
七、总结
Vue3的优化提升模块总结完毕,其实相较于Vue2而言,优化的地方还有更多,这篇博客主要侧重于编译优化的方面。在响应式等方面也有使用Proxy代理来进行性能优化,更有着依赖收集相关的优化算法更新。
本文出至:学新通技术网
标签: