写点什么

解读 Vue3 模板编译优化

作者:yyds2026
  • 2022 年 10 月 10 日
    浙江
  • 本文字数:5539 字

    阅读完需:约 18 分钟

今天的文章打算学习下 Vue3 下的模板编译与 Vue2 下的差异,以及 VDOM 下 Diff 算法的优化。

编译入口

了解过 Vue3 的同学肯定知道 Vue3 引入了新的组合 Api,在组件 mount 阶段会调用 setup 方法,之后会判断 render 方法是否存在,如果不存在会调用 compile 方法将 template 转化为 render


// packages/runtime-core/src/renderer.tsconst mountComponent = (initialVNode, container) => {  const instance = (    initialVNode.component = createComponentInstance(      // ...params    )  )  // 调用 setup  setupComponent(instance)}
// packages/runtime-core/src/component.tslet compileexport function registerRuntimeCompiler(_compile) { compile = _compile}export function setupComponent(instance) { const Component = instance.type const { setup } = Component if (setup) { // ...调用 setup } if (compile && Component.template && !Component.render) { // 如果没有 render 方法 // 调用 compile 将 template 转为 render 方法 Component.render = compile(Component.template, {...}) }}
复制代码


这部分都是 runtime-core 中的代码,之前的文章有讲过 Vue 分为完整版和 runtime 版本。如果使用 vue-loader 处理 .vue 文件,一般都会将 .vue 文件中的 template 直接处理成 render 方法。


//  需要编译器Vue.createApp({  template: '<div>{{ hi }}</div>'})
// 不需要Vue.createApp({ render() { return Vue.h('div', {}, this.hi) }})
复制代码


完整版与 runtime 版的差异就是,完整版会引入 compile 方法,如果是 vue-cli 生成的项目就会抹去这部分代码,将 compile 过程都放到打包的阶段,以此优化性能。runtime-dom 中提供了 registerRuntimeCompiler 方法用于注入 compile 方法。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sj0Ctvim-1665390661650)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c3b22494e2764f41afbc0692667e2dff~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]

主流程

在完整版的 index.js 中,调用了 registerRuntimeCompilercompile 进行注入,接下来我们看看注入的 compile 方法主要做了什么。


// packages/vue/src/index.tsimport { compile } from '@vue/compiler-dom'
// 编译缓存const compileCache = Object.create(null)
// 注入 compile 方法function compileToFunction( // 模板 template: string | HTMLElement, // 编译配置 options?: CompilerOptions): RenderFunction { if (!isString(template)) { // 如果 template 不是字符串 // 则认为是一个 DOM 节点,获取 innerHTML if (template.nodeType) { template = template.innerHTML } else { return NOOP } }
// 如果缓存中存在,直接从缓存中获取 const key = template const cached = compileCache[key] if (cached) { return cached }
// 如果是 ID 选择器,这获取 DOM 元素后,取 innerHTML if (template[0] === '#') { const el = document.querySelector(template) template = el ? el.innerHTML : '' }
// 调用 compile 获取 render code const { code } = compile( template, options )
// 将 render code 转化为 function const render = new Function(code)();
// 返回 render 方法的同时,将其放入缓存 return (compileCache[key] = render)}
// 注入 compileregisterRuntimeCompiler(compileToFunction)
复制代码


在讲 Vue2 模板编译的时候已经讲过,compile 方法主要分为三步,Vue3 的逻辑类似:


  1. 模板编译,将模板代码转化为 AST;

  2. 优化 AST,方便后续虚拟 DOM 更新;

  3. 生成代码,将 AST 转化为可执行的代码;参考 vue 实战视频讲解:进入学习


// packages/compiler-dom/src/index.tsimport { baseCompile, baseParse } from '@vue/compiler-core'export function compile(template, options) {  return baseCompile(template, options)}
// packages/compiler-core/src/compile.tsimport { baseParse } from './parse'import { transform } from './transform'
import { transformIf } from './transforms/vIf'import { transformFor } from './transforms/vFor'import { transformText } from './transforms/transformText'import { transformElement } from './transforms/transformElement'
import { transformOn } from './transforms/vOn'import { transformBind } from './transforms/vBind'import { transformModel } from './transforms/vModel'
export function baseCompile(template, options) { // 解析 html,转化为 ast const ast = baseParse(template, options) // 优化 ast,标记静态节点 transform(ast, { ...options, nodeTransforms: [ transformIf, transformFor, transformText, transformElement, // ... 省略了部分 transform ], directiveTransforms: { on: transformOn, bind: transformBind, model: transformModel } }) // 将 ast 转化为可执行代码 return generate(ast, options)}
复制代码

计算 PatchFlag

这里大致的逻辑与之前的并没有多大的差异,主要是 optimize 方法变成了 transform 方法,而且默认会对一些模板语法进行 transform。这些 transform 就是后续虚拟 DOM 优化的关键,我们先看看 transform 的代码 。


// packages/compiler-core/src/transform.tsexport function transform(root, options) {  const context = createTransformContext(root, options)  traverseNode(root, context)}export function traverseNode(node, context) {  context.currentNode = node  const { nodeTransforms } = context  const exitFns = []  for (let i = 0; i < nodeTransforms.length; i++) {    // Transform 会返回一个退出函数,在处理完所有的子节点后再执行    const onExit = nodeTransforms[i](node, context)    if (onExit) {      if (isArray(onExit)) {        exitFns.push(...onExit)      } else {        exitFns.push(onExit)      }    }  }  traverseChildren(node, context)  context.currentNode = node  // 执行所以 Transform 的退出函数  let i = exitFns.length  while (i--) {    exitFns[i]()  }}
复制代码


我们重点看一下 transformElement 的逻辑:


// packages/compiler-core/src/transforms/transformElement.tsexport const transformElement: NodeTransform = (node, context) => {  // transformElement 没有执行任何逻辑,而是直接返回了一个退出函数  // 说明 transformElement 需要等所有的子节点处理完后才执行  return function postTransformElement() {    const { tag, props } = node
let vnodeProps let vnodePatchFlag const vnodeTag = node.tagType === ElementTypes.COMPONENT ? resolveComponentType(node, context) : `"${tag}"`
let patchFlag = 0 // 检测节点属性 if (props.length > 0) { // 检测节点属性的动态部分 const propsBuildResult = buildProps(node, context) vnodeProps = propsBuildResult.props patchFlag = propsBuildResult.patchFlag }
// 检测子节点 if (node.children.length > 0) { if (node.children.length === 1) { const child = node.children[0] // 检测子节点是否为动态文本 if (!getStaticType(child)) { patchFlag |= PatchFlags.TEXT } } }
// 格式化 patchFlag if (patchFlag !== 0) { vnodePatchFlag = String(patchFlag) }
node.codegenNode = createVNodeCall( context, vnodeTag, vnodeProps, vnodeChildren, vnodePatchFlag ) }}
复制代码


buildProps 会对节点的属性进行一次遍历,由于内部源码涉及很多其他的细节,这里的代码是经过简化之后的,只保留了 patchFlag 相关的逻辑。


export function buildProps(  node: ElementNode,  context: TransformContext,  props: ElementNode['props'] = node.props) {  let patchFlag = 0  for (let i = 0; i < props.length; i++) {    const prop = props[i]    const [key, name] = prop.name.split(':')    if (key === 'v-bind' || key === '') {      if (name === 'class') {          // 如果包含 :class 属性,patchFlag | CLASS        patchFlag |= PatchFlags.CLASS      } else if (name === 'style') {          // 如果包含 :style 属性,patchFlag | STYLE        patchFlag |= PatchFlags.STYLE      }    }  }
return { patchFlag }}
复制代码


上面的代码只展示了三种 patchFlag 的类型:


  • 节点只有一个文本子节点,且该文本包含动态的数据TEXT = 1


<p>name: {{name}}</p>
复制代码


  • 节点包含可变的 class 属性CLASS = 1 << 1


<div :class="{ active: isActive }"></div>
复制代码


  • 节点包含可变的 style 属性STYLE = 1 << 2


<div :style="{ color: color }"></div>
复制代码


可以看到 PatchFlags 都是数字 1 经过 左移操作符 计算得到的。


export const enum PatchFlags {  TEXT = 1,             // 1, 二进制 0000 0001  CLASS = 1 << 1,       // 2, 二进制 0000 0010  STYLE = 1 << 2,       // 4, 二进制 0000 0100  PROPS = 1 << 3,       // 8, 二进制 0000 1000  ...}
复制代码


从上面的代码能看出来,patchFlag 的初始值为 0,每次对 patchFlag 都是执行 | (或)操作。如果当前节点是一个只有动态文本子节点且同时具有动态 style 属性,最后得到的 patchFlag 为 5(二进制:0000 0101)。


<p :style="{ color: color }">name: {{name}}</p>
复制代码


patchFlag = 0patchFlag |= PatchFlags.STYLEpatchFlag |= PatchFlags.TEXT// 或运算:两个对应的二进制位中只要一个是1,结果对应位就是1。// 0000 0001// 0000 0100// ------------// 0000 0101  =>  十进制 5
复制代码


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MDpZgiyc-1665390661653)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e3d09ac5b9a44f9497bd6ad32f888cce~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]


我们将上面的代码放到 Vue3 中运行:


const app = Vue.createApp({  data() {    return {      color: 'red',      name: 'shenfq'    }  },  template: `<div>      <p :style="{ color: color }">name: {{name}}</p>  </div>`})
app.mount('#app')
复制代码


最后生成的 render 方法如下,和我们之前的描述基本一致。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z6O1fmVz-1665390661655)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/988545d612924fc48b7e033f6a24df81~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]

render 优化

Vue3 在虚拟 DOM Diff 时,会取出 patchFlag 和需要进行的 diff 类型进行 &(与)操作,如果结果为 true 才进入对应的 diff。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6qlLYmPe-1665390661657)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cade0135939e430a9c926a6f822b129c~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]


还是拿之前的模板举例:


<p :style="{ color: color }">name: {{name}}</p>
复制代码


如果此时的 name 发生了修改,p 节点进入了 diff 阶段,此时会将判断 patchFlag & PatchFlags.TEXT ,这个时候结果为真,表明 p 节点存在文本修改的情况。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tzwu7A4S-1665390661659)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5596ad51debe49b2a8eecec24a9eeda8~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]


patchFlag = 5patchFlag & PatchFlags.TEXT// 或运算:只有对应的两个二进位都为1时,结果位才为1。// 0000 0101// 0000 0001// ------------// 0000 0001  =>  十进制 1
复制代码


if (patchFlag & PatchFlags.TEXT) {  if (oldNode.children !== newNode.children) {    // 修改文本    hostSetElementText(el, newNode.children)  }}
复制代码


但是进行 patchFlag & PatchFlags.CLASS 判断时,由于节点并没有动态 Class,返回值为 0,所以就不会对该节点的 class 属性进行 diff,以此来优化性能。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O6SMFmwd-1665390661661)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/01c4a1adc4a342079be07ddc88406b95~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]


patchFlag = 5patchFlag & PatchFlags.CLASS// 或运算:只有对应的两个二进位都为1时,结果位才为1。// 0000 0101// 0000 0010// ------------// 0000 0000  =>  十进制 0
复制代码

总结

其实 Vue3 相关的性能优化有很多,这里只单独将 patchFlag 的十分之一的内容拿出来讲了,Vue3 还没正式发布的时候就有看到说 Diff 过程会通过 patchFlag 来进行性能优化,所以打算看看他的优化逻辑,总的来说还是有所收获。

用户头像

yyds2026

关注

还未添加个人签名 2022.09.08 加入

还未添加个人简介

评论

发布
暂无评论
解读Vue3模板编译优化_Vue_yyds2026_InfoQ写作社区