编译优化
是什么?
编译优化 指的是编译器将 模板(template) 编译为 渲染函数(render) 的过程中,尽可能的 提取关键信息,以达到 生成最优代码 的过程。
为什么需要?
传统的 Diff 算法会存在很多无意义的对比操作
在对比 新旧 两颗 虚拟 DOM 时,总是要按照 虚拟 DOM 的 层级结构 "一层一层" 进行遍历,然后其中某些内容的遍历对比是完全没必要的,例如:
<div id="foo"> <p class="bar">{{ text }}</p> </div> 复制代码
其中唯一可能变化的就是 <p>
标签中的 text
值,当响应式数据 text
发生修改时,最高效的更新方式就是直接更新 <p>
标签对应的文本内容,然而对于 传统 Diff 算法 而言,会先根据 render
函数生成 新的虚拟 DOM,然后再对比 新旧虚拟 DOM 的方式:
- 对比
div
节点,以及该节点的属性和子节点 - 对比
p
节点,以及该节点的属性和子节点 - 对比
p
节点的文本节点,发现文本节点发生变化,于是更新文本节点
编译思路
Vue.js3 编译优化的思路来源就是,跳过这些无意义的对操作,进一步的提升 Vue 中 Diff 算法的对比性能:
- 模板的结构相对稳定,在编译阶段尽可能提取关键信息(如:标记静态节点、动态节点)
- 基于关键信息,通过编译器直接生成对应的原生 DOM 操作代码,减少生成 虚拟 DOM 的性能消耗,有利于提升初始化渲染的速度
实验性的新编译策略
从理论上来看,某些情况下确实并不需要 虚拟 DOM,(即 No Virtual DOM),但在 Vue.js3 中仍然选择保留虚拟 DOM,并承受其带来的性能开销,主要是考虑到 渲染函数的灵活性 和 Vue.js2 的兼容性 问题.
感兴趣可以去了解下,未来 Vue 会提供的一些新特性,不过这并不是本文的核心内容,State of Vue 2022-尤雨溪
Vue3 中的编译优化的方式
标记动态节点
标记动态节点之后,在后续渲染器更新阶段旧可以直接基于动态节点集合,实现对动态节点的 靶向更新 或 定向更新.
patchFlag 属性
在编译器进行编译时,如果判断当前节点是属于 动态节点,就会为这个 vnode
节点打上 patchFlag 标记
,也就是添加一个 patchFlag 属性
,并且 patchFlag 属性
对应的 数值 代表了当前这个 动态节点的类型,如:
- 数字
1
:代表该节点是 动态 的textContent
- 数字
2
:代表该节点是 动态 的calss 绑定
- 数字
3
:代表该节点是 动态 的style 绑定
- ...
dynamicChildren 属性
dynamicChildren
属性 值对应的是一个数组,其中保存的就是带有 patchFlag
属性 的 vnode
节点,并且带有 dynamicChildren
属性 的 vnode
节点成称为 块,即 Block.
Block 节点
一个 Block 本质上也是一个 虚拟 DOM
节点,只不过它比普通的虚拟节点多了一个用于 存储动态子节点 的 dynamicChildren
属性.
一个 Block 不仅能够收集它的 直接动态子节点
,也能收集所有 动态的子代节点
,而后续渲染器的更新操作将以 Block 作为更新维度去处理.
什么样的节点会变成 Block 节点?
- 所有模板的 根节点
- 带有
v-if
指令的节点 - 带有
v-for
指令的节点 - 模板中
Frament
节点所包裹的 多根节点
其中 v-if
和 v-for
指令会导致 更新前后模板结构不稳定,不过由于 v-for
指令渲染的是一组子节点,为了更好的表示这一组子节点,就需要使用 Fragment 节点来表达 v-for
指令的渲染结果,并将其作为 Block 节点.
静态提升
静态提升的目的是尽可能减少更新时创建 虚拟 DOM 带来的 性能开销 和 内存占用.
没有静态提升时带来的问题
通常,在响应式数据发生变化时,渲染函数就会重新执行,并产生新的虚拟 DOM 节点,显然纯静态的虚拟节点完全没有必要重新创建,这会带来一定的性能开销.
解决方案
在编译阶段可以 将纯静态节点提升到渲染函数外部,在渲染函数内部保持对静态节点的引用即可,当响应式数据变化引起渲染函数重新执行时,并不会重新创建静态的虚拟节点,这样旧可以避免重复创建静态节点的虚拟 DOM 带来的性能开销.
值得注意的是,静态提升是以树为单位的,毕竟不可能会为每一个小的静态节点进行静态提升,这会导致渲染函数外部对应存储静态节点的变量增多,这也会 占用一定的内存.
预字符串化
基于 静态提升 可以继续采用 预字符串化 的优化手段,即直接将原本需要以树为单位进行静态提升的内容,直接转换为对应基于 DOM 操作的 字符串形式.
预字符串化的优势如下:
- 大块的静态内容可以直接通过
innerHTML
进行设置,在 初始化渲染 时具有一定的性能优势 - 减少创建虚拟节点产生开销的性能
- 减少内存占用
缓存内联事件处理函数
不缓存内联事件函数带来的问题
在模板事件处理函数中,为了一些简单的更新操作,通常会在模板中编写 内联的事件处理函数,例如:
<Comp @change="c = a + b"> ===> function render(ctx){ return h(Com, { // 内联事件处理函数 onChange: () => ctx.c = ctx.a + ctx.b }) } 复制代码
显然,当 render
函数被重新执行时,都为会 Comp
组件创建一个全新的 props
对象,并且其中的 onChange
事件也是一个全新的函数,这会导致渲染器对 Comp
组件进行更新,造成额外的性能开销。
解决方案
通过为 render
渲染函数传递第二个参数 cache
数组,且这个 cache
数组是来自于组件实例的,因此可以将内联事件处理函数添加到 cache
数组中缓存起来.
当渲染函数重新执行时并创建虚拟 DOM
时,优先从缓存中读取对应的事件处理函数,避免事件处理函数被重新创建,导致 Comp
组件进行无用更新.
v-once 缓存虚拟 DOM
Vue.js2 和 Vue.js3 中都支持 v-once
指令,当前编译器遇到 v-once
指令时,会利用上面提到的 cache
数组来缓存渲染函数的全部或部分执行结果.
v-once 的优势
- 避免组件更新时重新创建虚拟 DOM 带来的性能开销,因为虚拟 DOM 被缓存了,因此更新时无需重新创建
- 避免无用的 Diff 开销,这是因为被
v-once
标记的虚拟 DOM 树会被父级 Block 节点收集