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
中,调用了 registerRuntimeCompiler
将 compile
进行注入,接下来我们看看注入的 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 的逻辑类似:
-
模板编译,将模板代码转化为 AST; -
优化 AST,方便后续虚拟 DOM 更新; -
生成代码,将 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
我们将上面的代码放到 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
方法如下,和我们之前的描述基本一致。
render 优化
Vue3 在虚拟 DOM Diff 时,会取出 patchFlag
和需要进行的 diff 类型进行 &
(与)操作,如果结果为 true 才进入对应的 diff。
还是拿之前的模板举例:
<p :style="{ color: color }">name: {{name}}</p>
如果此时的 name 发生了修改,p 节点进入了 diff 阶段,此时会将判断 patchFlag & PatchFlags.TEXT
,这个时候结果为真,表明 p 节点存在文本修改的情况。
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 = 5
patchFlag & PatchFlags.CLASS
// 或运算:只有对应的两个二进位都为1时,结果位才为1。
// 0000 0101
// 0000 0010
// ------------
// 0000 0000 => 十进制 0
总结
其实 Vue3 相关的性能优化有很多,这里只单独将 patchFlag 的十分之一的内容拿出来讲了,Vue3 还没正式发布的时候就有看到说 Diff 过程会通过 patchFlag 来进行性能优化,所以打算看看他的优化逻辑,总的来说还是有所收获。
本文分享自微信公众号 - 更了不起的前端(shenfq777)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
跟坚哥学QUIC系列:版本协商(Version Negotiation)
QUIC 目前由IETF 工作组起草进行标准化设计,预计 2021 年初提交 RFC。IETF 工作组在设计过程中发布了多个版本的草案,目前最新的草案版本是 2020-10-20 发布的draft-32。另外 QUIC 是在用户侧(User space)实现的,版本迭代会比较方便和快速,市面上的QUIC 协议的实现可能基于不同的版本(比如 draft-29, draft-30, ...)。这意味着客户端(client)和服务端(server)支持的 QUIC 协议版本可能不一样,它们建立连接时需要先进行版本协商(Version Negotiation),使用双方都支持的一个版本。 客户端和服务端创建连接时,客户端在首次发起请求时需要带上它支持的协议版本号。 发送版本协商数据包 当服务端收到新连接的数据包时,它会检查是否支持客户端的协议版本: 如果服务端可以支持客户端的版本, 服务端将为连接的整个生命周期使用这个协议版本。 如果服务端不支持该版本,服务端就响应版本协商包(Version Negotiation packet),附上它所支持的版本集合,这将增加 1-RTT(Round-Tr...
- 下一篇
中国互联网反垄断强监管的时代到来了
loonggg 读完需要 3 分钟 速读仅需 1 分钟 这几天国内互联网公司股价一度下跌,原因很简单,就是因为国家出台了《关于平台经济领域的反垄断指南(征求意见稿》,从 2008 年国内《反垄断法》正式实施到 2020 年《关于平台经济领域的反垄断指南(征求意见稿》的出台,或许正在标志着国内互联网正式进入反垄断的强监管时代。 这两天国内互联网平台的股价情况:腾讯合计-11.49%,阿里-11.14%,美团-19.15%,京东-17.17%,拼多多-9.99%,小米-12.13%。恒生科技指数重挫超 6%,昨天跌幅是 5%,堪称是局部股灾了。 其实从 2008 年《反垄断法》实施以来,到 2020 年,这 12 年间,几乎很少有对互联网公司的垄断调查。据我能够查到的数据,据统计,到 2018 年,10 年来,依法打击垄断行为,查结垄断协议案 163 件和滥用市场支配地位案 54 件,累计罚款金额超过 110 亿元人民币,查结滥用行政 - 权力排除、限制竞争案 183 件,震慑了违法者,净化了市场环境;审结经营者集中案件 2283 件,查处未依法申报案件 22 件,有效预防市场垄断,维护市...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Docker安装Oracle12C,快速搭建Oracle学习环境
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS关闭SELinux安全模块
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Hadoop3单机部署,实现最简伪集群