Vue3编译优化之PatchFlags和ShapeFlags

3,475 阅读4分钟

自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

1641279424(1).png

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过程为全量对比

1641281648(1).png

我们看到,在发生数据更新的时候,只有p标签绑定的msg为动态值,可是在进行新旧VNode diff过程中,不会发生变化的节点也会参与其中进行完整的VNode树进行diff。

2. 2 vue3中的静态标记在diff过程的优化

在vue3中,由于在compile阶段对VNode每一个元素做了对应的PatchFlags进行标记,所以在diff过程中,我们就可以根据具体哪些发生了变化,进行有目标的diff实现靶向更新,这正是vue3中compile和runtime两个阶段的巧妙结合之处。

1641282238(1).png

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算法,实现靶向更新

最后,文章如有解释不对之处,欢迎评论区交流,如果感觉有所收获,还请点赞支持!