聊聊Vue3 的模板编译优化

开发 前端
Vue3 正式发布已经有一段时间了,前段时间写了一篇文章(《Vue 模板编译原理》)分析 Vue 的模板编译原理。今天的文章打算学习下 Vue3 下的模板编译与 Vue2 下的差异,以及 VDOM 下 Diff 算法的优化。

[[351771]]

Vue3 正式发布已经有一段时间了,前段时间写了一篇文章(《Vue 模板编译原理》)分析 Vue 的模板编译原理。今天的文章打算学习下 Vue3 下的模板编译与 Vue2 下的差异,以及 VDOM 下 Diff 算法的优化。

编译入口

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

  1. // packages/runtime-core/src/renderer.ts 
  2. const mountComponent = (initialVNode, container) => { 
  3.   const instance = ( 
  4.     initialVNode.component = createComponentInstance( 
  5.       // ...params 
  6.     ) 
  7.   ) 
  8.   // 调用 setup 
  9.   setupComponent(instance) 
  10.  
  11. // packages/runtime-core/src/component.ts 
  12. let compile 
  13. export function registerRuntimeCompiler(_compile) { 
  14.   compile = _compile 
  15. export function setupComponent(instance) { 
  16.   const Component = instance.type 
  17.   const { setup } = Component 
  18.   if (setup) { 
  19.     // ...调用 setup 
  20.   } 
  21.   if (compile && Component.template && !Component.render) { 
  22.    // 如果没有 render 方法 
  23.     // 调用 compile 将 template 转为 render 方法 
  24.     Component.render = compile(Component.template, {...}) 
  25.   } 

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

  1. //  需要编译器 
  2. Vue.createApp({ 
  3.   template: '<div>{{ hi }}</div>' 
  4. }) 
  5.  
  6. // 不需要 
  7. Vue.createApp({ 
  8.   render() { 
  9.     return Vue.h('div', {}, this.hi) 
  10.   } 
  11. }) 

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

主流程

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

  1. // packages/vue/src/index.ts 
  2. import { compile } from '@vue/compiler-dom' 
  3.  
  4. // 编译缓存 
  5. const compileCache = Object.create(null
  6.  
  7. // 注入 compile 方法 
  8. function compileToFunction( 
  9.  // 模板 
  10.   template: string | HTMLElement, 
  11.   // 编译配置 
  12.   options?: CompilerOptions 
  13. ): RenderFunction { 
  14.   if (!isString(template)) { 
  15.     // 如果 template 不是字符串 
  16.     // 则认为是一个 DOM 节点,获取 innerHTML 
  17.     if (template.nodeType) { 
  18.       template = template.innerHTML 
  19.     } else { 
  20.       return NOOP 
  21.     } 
  22.   } 
  23.  
  24.   // 如果缓存中存在,直接从缓存中获取 
  25.   const key = template 
  26.   const cached = compileCache[key
  27.   if (cached) { 
  28.     return cached 
  29.   } 
  30.  
  31.   // 如果是 ID 选择器,这获取 DOM 元素后,取 innerHTML 
  32.   if (template[0] === '#') { 
  33.     const el = document.querySelector(template) 
  34.     template = el ? el.innerHTML : '' 
  35.   } 
  36.  
  37.   // 调用 compile 获取 render code 
  38.   const { code } = compile( 
  39.     template, 
  40.     options 
  41.   ) 
  42.  
  43.   // 将 render code 转化为 function 
  44.   const render = new Function(code)(); 
  45.  
  46.  // 返回 render 方法的同时,将其放入缓存 
  47.   return (compileCache[key] = render) 
  48.  
  49. // 注入 compile 
  50. registerRuntimeCompiler(compileToFunction) 

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

  1. 模板编译,将模板代码转化为 AST;
  2. 优化 AST,方便后续虚拟 DOM 更新;
  3. 生成代码,将 AST 转化为可执行的代码;
  1. // packages/compiler-dom/src/index.ts 
  2. import { baseCompile, baseParse } from '@vue/compiler-core' 
  3. export function compile(template, options) { 
  4.   return baseCompile(template, options) 
  5.  
  6. // packages/compiler-core/src/compile.ts 
  7. import { baseParse } from './parse' 
  8. import { transform } from './transform' 
  9.  
  10. import { transformIf } from './transforms/vIf' 
  11. import { transformFor } from './transforms/vFor' 
  12. import { transformText } from './transforms/transformText' 
  13. import { transformElement } from './transforms/transformElement' 
  14.  
  15. import { transformOn } from './transforms/vOn' 
  16. import { transformBind } from './transforms/vBind' 
  17. import { transformModel } from './transforms/vModel' 
  18.  
  19. export function baseCompile(template, options) { 
  20.   // 解析 html,转化为 ast 
  21.   const ast = baseParse(template, options) 
  22.   // 优化 ast,标记静态节点 
  23.   transform(ast, { 
  24.     ...options, 
  25.     nodeTransforms: [ 
  26.       transformIf, 
  27.       transformFor, 
  28.       transformText, 
  29.       transformElement, 
  30.       // ... 省略了部分 transform 
  31.     ], 
  32.     directiveTransforms: { 
  33.       on: transformOn, 
  34.       bind: transformBind, 
  35.       model: transformModel 
  36.     } 
  37.   }) 
  38.   // 将 ast 转化为可执行代码 
  39.   return generate(ast, options) 

计算 PatchFlag

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

  1. // packages/compiler-core/src/transform.ts 
  2. export function transform(root, options) { 
  3.   const context = createTransformContext(root, options) 
  4.   traverseNode(root, context) 
  5. export function traverseNode(node, context) { 
  6.   context.currentNode = node 
  7.   const { nodeTransforms } = context 
  8.   const exitFns = [] 
  9.   for (let i = 0; i < nodeTransforms.length; i++) { 
  10.     // Transform 会返回一个退出函数,在处理完所有的子节点后再执行 
  11.     const onExit = nodeTransforms[i](node, context) 
  12.     if (onExit) { 
  13.       if (isArray(onExit)) { 
  14.         exitFns.push(...onExit) 
  15.       } else { 
  16.         exitFns.push(onExit) 
  17.       } 
  18.     } 
  19.   } 
  20.   traverseChildren(node, context) 
  21.   context.currentNode = node 
  22.   // 执行所以 Transform 的退出函数 
  23.   let i = exitFns.length 
  24.   while (i--) { 
  25.     exitFns[i]() 
  26.   } 

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

  1. // packages/compiler-core/src/transforms/transformElement.ts 
  2. export const transformElement: NodeTransform = (node, context) => { 
  3.   // transformElement 没有执行任何逻辑,而是直接返回了一个退出函数 
  4.   // 说明 transformElement 需要等所有的子节点处理完后才执行 
  5.   return function postTransformElement() { 
  6.     const { tag, props } = node 
  7.  
  8.     let vnodeProps 
  9.     let vnodePatchFlag 
  10.     const vnodeTag = node.tagType === ElementTypes.COMPONENT 
  11.       ? resolveComponentType(node, context) 
  12.       : `"${tag}"
  13.      
  14.     let patchFlag = 0 
  15.     // 检测节点属性 
  16.     if (props.length > 0) { 
  17.       // 检测节点属性的动态部分 
  18.       const propsBuildResult = buildProps(node, context) 
  19.       vnodeProps = propsBuildResult.props 
  20.       patchFlag = propsBuildResult.patchFlag 
  21.     } 
  22.  
  23.     // 检测子节点 
  24.     if (node.children.length > 0) { 
  25.       if (node.children.length === 1) { 
  26.         const child = node.children[0] 
  27.         // 检测子节点是否为动态文本 
  28.         if (!getStaticType(child)) { 
  29.           patchFlag |= PatchFlags.TEXT 
  30.         } 
  31.       } 
  32.     } 
  33.  
  34.     // 格式化 patchFlag 
  35.     if (patchFlag !== 0) { 
  36.         vnodePatchFlag = String(patchFlag) 
  37.     } 
  38.  
  39.     node.codegenNode = createVNodeCall( 
  40.       context, 
  41.       vnodeTag, 
  42.       vnodeProps, 
  43.       vnodeChildren, 
  44.       vnodePatchFlag 
  45.     ) 
  46.   } 

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

  1. export function buildProps( 
  2.   node: ElementNode, 
  3.   context: TransformContext, 
  4.   props: ElementNode['props'] = node.props 
  5. ) { 
  6.   let patchFlag = 0 
  7.   for (let i = 0; i < props.length; i++) { 
  8.     const prop = props[i] 
  9.     const [keyname] = prop.name.split(':'
  10.     if (key === 'v-bind' || key === '') { 
  11.       if (name === 'class') { 
  12.        // 如果包含 :class 属性,patchFlag | CLASS 
  13.         patchFlag |= PatchFlags.CLASS 
  14.       } else if (name === 'style') { 
  15.        // 如果包含 :style 属性,patchFlag | STYLE 
  16.         patchFlag |= PatchFlags.STYLE 
  17.       } 
  18.     } 
  19.   } 
  20.  
  21.   return { 
  22.     patchFlag 
  23.   } 

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

  • 节点只有一个文本子节点,且该文本包含动态的数据(TEXT = 1)
  1. <p>name: {{name}}</p> 
  • 节点包含可变的 class 属性(CLASS = 1 << 1)
    1. <div :class="{ active: isActive }"></div> 

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

  1. <div :style="{ color: color }"></div> 

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

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

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

  1. <p :style="{ color: color }">name: {{name}}</p> 

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

  1. const app = Vue.createApp({ 
  2.   data() { 
  3.     return { 
  4.       color: 'red'
  5.       name'shenfq' 
  6.     } 
  7.   }, 
  8.   template: `<div> 
  9.    <p :style="{ color: color }">name: {{name}}</p> 
  10.   </div>` 
  11. }) 
  12.  
  13. app.mount('#app'

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

function render() {}

render 优化

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

patchFlag 判断

还是拿之前的模板举例:

  1. <p :style="{ color: color }">name: {{name}}</p> 

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

patchFlag

  1. patchFlag = 5 
  2. patchFlag & PatchFlags.TEXT 
  3. // 或运算:只有对应的两个二进位都为1时,结果位才为1。 
  4. // 0000 0101 
  5. // 0000 0001 
  6. // ------------ 
  7. // 0000 0001  =>  十进制 1 
  1. if (patchFlag & PatchFlags.TEXT) { 
  2.   if (oldNode.children !== newNode.children) { 
  3.     // 修改文本 
  4.     hostSetElementText(el, newNode.children) 
  5.   } 

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

patchFlag

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

总结

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

本文转载自微信公众号「更了不起的前端」,可以通过以下二维码关注。转载本文请联系更了不起的前端公众号。

 

责任编辑:武晓燕 来源: 更了不起的前端
相关推荐

2021-01-15 05:16:37

Vue3开源代码量

2020-09-14 08:56:30

Vue模板

2021-12-01 08:11:44

Vue3 插件Vue应用

2021-11-30 08:19:43

Vue3 插件Vue应用

2023-11-28 09:03:59

Vue.jsJavaScript

2021-12-02 05:50:35

Vue3 插件Vue应用

2020-09-19 21:15:26

Composition

2021-12-08 09:09:33

Vue 3 Computed Vue2

2021-05-12 08:57:56

项目搭建工具

2022-06-21 12:09:18

Vue差异

2021-05-26 10:40:28

Vue3TypeScript前端

2021-11-16 08:50:29

Vue3 插件Vue应用

2022-03-10 11:04:04

Vue3Canvas前端

2020-12-01 08:34:31

Vue3组件实践

2024-02-28 08:35:26

内置组件Vue3页面

2023-04-28 08:35:22

Vue 3Vue 2

2021-11-26 05:59:31

Vue3 插件Vue应用

2021-12-29 07:51:21

Vue3 插件Vue应用

2024-03-22 08:57:04

Vue3Emoji表情符号

2022-11-01 11:55:27

ReactVue3
点赞
收藏

51CTO技术栈公众号