Vue3 模板编译优化

Shenfq 2020-11-11 15:51:59
优化 vue 编译 模板 vue3


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

编译入口

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

// packages/runtime-core/src/renderer.ts
const mountComponent = (initialVNode, container) => {
const instance = (
initialVNode.component = createComponentInstance(
// ...params
)
)
// 调用 setup
setupComponent(instance)
}
// packages/runtime-core/src/component.ts
let compile
export 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 方法。

主流程

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

// packages/vue/src/index.ts
import { 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)
}
// 注入 compile
registerRuntimeCompiler(compileToFunction)

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

  1. 模板编译,将模板代码转化为 AST;
  2. 优化 AST,方便后续虚拟 DOM 更新;
  3. 生成代码,将 AST 转化为可执行的代码;
// packages/compiler-dom/src/index.ts
import { baseCompile, baseParse } from '@vue/compiler-core'
export function compile(template, options) {
return baseCompile(template, options)
}
// packages/compiler-core/src/compile.ts
import { 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.ts
export 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.ts
export 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 = 0
patchFlag |= PatchFlags.STYLE
patchFlag |= PatchFlags.TEXT
// 或运算:两个对应的二进制位中只要一个是1,结果对应位就是1。
// 0000 0001
// 0000 0100
// ------------
// 0000 0101 => 十进制 5

patchFlag

我们将上面的代码放到 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 方法如下,和我们之前的描述基本一致。

function render() {}

render 优化

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

patchFlag 判断

还是拿之前的模板举例:

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

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

patchFlag

patchFlag = 5
patchFlag & 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,以此来优化性能。

patchFlag

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

总结

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

版权声明
本文为[Shenfq]所创,转载请带上原文链接,感谢
https://segmentfault.com/a/1190000037800237

  1. [front end -- JavaScript] knowledge point (IV) -- memory leakage in the project (I)
  2. This mechanism in JS
  3. Vue 3.0 source code learning 1 --- rendering process of components
  4. Learning the realization of canvas and simple drawing
  5. gin里获取http请求过来的参数
  6. vue3的新特性
  7. Get the parameters from HTTP request in gin
  8. New features of vue3
  9. vue-cli 引入腾讯地图(最新 api,rocketmq原理面试
  10. Vue 学习笔记(3,免费Java高级工程师学习资源
  11. Vue 学习笔记(2,Java编程视频教程
  12. Vue cli introduces Tencent maps (the latest API, rocketmq)
  13. Vue learning notes (3, free Java senior engineer learning resources)
  14. Vue learning notes (2, Java programming video tutorial)
  15. 【Vue】—props属性
  16. 【Vue】—创建组件
  17. [Vue] - props attribute
  18. [Vue] - create component
  19. 浅谈vue响应式原理及发布订阅模式和观察者模式
  20. On Vue responsive principle, publish subscribe mode and observer mode
  21. 浅谈vue响应式原理及发布订阅模式和观察者模式
  22. On Vue responsive principle, publish subscribe mode and observer mode
  23. Xiaobai can understand it. It only takes 4 steps to solve the problem of Vue keep alive cache component
  24. Publish, subscribe and observer of design patterns
  25. Summary of common content added in ES6 + (II)
  26. No.8 Vue element admin learning (III) vuex learning and login method analysis
  27. Write a mini webpack project construction tool
  28. Shopping cart (front-end static page preparation)
  29. Introduction to the fluent platform
  30. Webpack5 cache
  31. The difference between drop-down box select option and datalist
  32. CSS review (III)
  33. Node.js学习笔记【七】
  34. Node.js learning notes [VII]
  35. Vue Router根据后台数据加载不同的组件(思考-&gt;实现-&gt;不止于实现)
  36. Vue router loads different components according to background data (thinking - & gt; Implementation - & gt; (more than implementation)
  37. 【JQuery框架,Java编程教程视频下载
  38. [jQuery framework, Java programming tutorial video download
  39. Vue Router根据后台数据加载不同的组件(思考-&gt;实现-&gt;不止于实现)
  40. Vue router loads different components according to background data (thinking - & gt; Implementation - & gt; (more than implementation)
  41. 【Vue,阿里P8大佬亲自教你
  42. 【Vue基础知识总结 5,字节跳动算法工程师面试经验
  43. [Vue, Ali P8 teaches you personally
  44. [Vue basic knowledge summary 5. Interview experience of byte beating Algorithm Engineer
  45. 【问题记录】- 谷歌浏览器 Html生成PDF
  46. [problem record] - PDF generated by Google browser HTML
  47. 【问题记录】- 谷歌浏览器 Html生成PDF
  48. [problem record] - PDF generated by Google browser HTML
  49. 【JavaScript】查漏补缺 —数组中reduce()方法
  50. [JavaScript] leak checking and defect filling - reduce() method in array
  51. 【重识 HTML (3),350道Java面试真题分享
  52. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  53. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  54. [re recognize HTML (3) and share 350 real Java interview questions
  55. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  56. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  57. 【重识 HTML ,nginx面试题阿里
  58. 【重识 HTML (4),ELK原来这么简单
  59. [re recognize HTML, nginx interview questions]
  60. [re recognize HTML (4). Elk is so simple