自2020年4月份Vue3发布3.0.0 beta1版本以来,Vue3一直在不断的迭代完善,目前最新的版本是3.2.26。在这不到一年的时间来,主流UI框架如Ant Design Vue、Element Plus、Vant也都率先推出了基于Vue3的版本。
Vue3底层基于TS语言对Vue2进行了重构的设计和开发,并保持向下兼容的特性,开发者可以基本无缝迁移到Vue3的新版本中来,Vue3相比与Vue2进行了许多性能优化之处,其中优化的关键之一就是重写了VNode。本篇文章就带大家深入了解下Vue3从编译(compile)到运行(runtime)的优化设计。
1. 关于PatchFlags静态标记
PatchFlags是Vue3在compile阶段(template模板创建渲染函数)在transform阶段,遍历AST Element,根据Element属性、子节点内容等信息添加的一个信息标识,用来标记一个Element部门有哪些是绑定了动态变量值,这样在runtime运行时patch阶段,可以更快的找出vnode tree哪些需要进行diff patch,实现靶向更新。
1. 1 vue2中的Compile
在vue2中,template模板被编译生成渲染函数,主要依据compileToFunctions方法
export const createCompiler = createCompilerCreator(function baseCompile (
template: string, // template内容
options: CompilerOptions // 平台相关节点操作方法
): CompiledResult {
// 第一步:解析template得到抽象语法树AST
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 第二步:优化原始AST,标记静态节点
optimize(ast, options)
}
// 第三步:将AST生成render函数
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
可以看到,vue2中的compile主要经历了3个步骤:
- parse: 解析template得到抽象语法树AST
- optimize: 优化原始AST,标记静态跟节点
- generate: 将AST生成可执行代码render函数
1. 2 vue3中的Compile
在vue3中,template模板被编译生成渲染函数主要在complie-core模块内baseCompile方法实现
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
// 判断 template 模板是否为字符串,如果是的话则会对字符串进行解析,否则直接将 template 作为 AST
const ast = isString(template) ? baseParse(template, options) : template
// 优化 ast,标记静态节点
transform(
ast,
extend({}, options, {})
)
// 将 ast 转化为可执行代码render 渲染函数
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
可以看到,在vue3中,将template转化为render函数,也经过了3个步骤:
- baseParse: 解析template得到抽象语法树AST
- transform: 优化原始AST,添加
PatchFlags静态节点标记 - generate: 将AST生成可执行代码render函数
1. 3 PatchFlags定义
再看下在vue3中对PatchFlags的定义描述
export const enum PatchFlags {
// 动态文本节点
TEXT = 1,
// 2 动态class
CLASS = 1 << 1,
// 4 动态style
STYLE = 1 << 2,
// 8 动态属性,但不好汉class style
PROPS = 1 << 3,
// 16 具有动态key属性,当key改变时,需要进行完整的diff
FULL_PROPS = 1 << 4,
// 32 带有监听事件的节点
HYDRATE_EVENTS = 1 << 5,
// 64 一个不会改变子节点顺序的fragment
STABLE_FRAGMENT = 1 << 6,
// 128 带有key的fragment
KEYED_FRAGMENT = 1 << 7,
// 256 没有key的fragment
UNKEYED_FRAGMENT = 1 << 8,
// 512 一个子节点只会进行非props比较
NEED_PATCH = 1 << 9,
// 1024 动态插槽
DYNAMIC_SLOTS = 1 << 10,
// 下面是特殊的,即在diff阶段会被跳过的
// 2048 表示仅因为用户在模板的根级别放置注释而创建的片段,这是一个仅用于开发的标志,因为注释在生产中被剥离
DEV_ROOT_FRAGMENT = 1 << 11,
// 静态节点,它的内容永远不会改变,不需要进行diff
HOISTED = -1,
// 用来表示一个节点的diff应该结束
BAIL = -2
}
我们看到,PatchFlags被定义为十几种的枚举类型,用以更精准的定位diff阶段需要对比节点部分,实现更精准的靶向更新。PatchFlags大致被分为了两类:
- 值大于0,即代表所对应的element在patch阶段,可以进行优化diff
- 值小于0,即代表所对应的element在patch阶段,不需要进行diff
这里可以参考官方的一个在线编译测试地址Vue 3 Template Explorer
2. 对比vue3和vue2中的diff差异
我们知道,VNode的出现初衷是减少对真实Dom的操作以提高页面节点更新的性能,但是这个减少也是带有成本的,即VNode在更新阶段patch进行diff过程的大量复杂递归计算,在一个大型的复杂应用中,存在着非常大量且复杂的关系VNode,在出现数据变化页面更新的过程中,需要不断递归patch,而这也是vue2的diff痛点。因为在vue2中的patch过程中,它是一个全量diff的过程。
我们以下面一段代码来举例说明
<div>
<h3>hello vue</h3>
<p>{{msg}}</p>
</div>
2. 1 vue2中的diff示例
vue2中的VNode在进行更新patch操作diff过程为全量对比
我们看到,在发生数据更新的时候,只有p标签绑定的msg为动态值,可是在进行新旧VNode diff过程中,不会发生变化的节点也会参与其中进行完整的VNode树进行diff。
2. 2 vue3中的静态标记在diff过程的优化
在vue3中,由于在compile阶段对VNode每一个元素做了对应的PatchFlags进行标记,所以在diff过程中,我们就可以根据具体哪些发生了变化,进行有目标的diff实现靶向更新,这正是vue3中compile和runtime两个阶段的巧妙结合之处。
3. ShapeFlags
在Vue3中的patch过程中,用到的另一个重要信息标识就是ShapeFlags,它主要用来定义描述组件的分类,在patch阶段中,依据不同的组件类型执行对应的处理逻辑。
export const enum ShapeFlags {
ELEMENT = 1, // HTML 或 SVG 标签 普通 DOM 元素
FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件
STATEFUL_COMPONENT = 1 << 2, // 普通有状态组件
TEXT_CHILDREN = 1 << 3, // 子节点为纯文本
ARRAY_CHILDREN = 1 << 4, // 子节点是数组
SLOTS_CHILDREN = 1 << 5, // 子节点是插槽
TELEPORT = 1 << 6, // Teleport
SUSPENSE = 1 << 7, // Supspense
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 需要被keep-live的有状态组件
COMPONENT_KEPT_ALIVE = 1 << 9, //已经被keep-live的有状态组件
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 有状态组件和函数组件都是组件,用component表示
}
4. 总结
以上便是与大家分享了Vue3中在编译时所做的优化:
- 静态节点标记
- 静态提升
- 事件缓存
也是得益于编译阶段的优化前提下,使得在VNode进行diff过程中优化diff算法,实现
靶向更新。
最后,文章如有解释不对之处,欢迎评论区交流,如果感觉有所收获,还请点赞支持!