Vue源码 编译(三)optimize 优化AST树
学习内容和文章内容来自 黄轶老师
黄轶老师的慕课网视频教程地址:
《Vue.js2.0 源码揭秘》、
黄轶老师拉钩教育教程地址:
《Vue.js 3.0 核心源码解析》
这里分析的源码是Runtime + Compiler 的 Vue.js
调试代码在:node_modules\vue\dist\vue.esm.js 里添加
vue版本:Vue.js 2.5.17-beta
你越是认真生活,你的生活就会越美好
——弗兰克·劳埃德·莱特
《人生果实》经典语录
Vue源码 编译(一)编译入口
我们找到的编译的入口
,它主要就是执行了如下几个逻辑:
解析模板字符串生成 AST
const ast = parse(template.trim(), options)
优化语法树
optimize(ast, options)
生成代码
const code = generate(ast, options)
optimize
optimize
的目标是通过标记静态根的方式
,优化重新渲染过程中对静态节点的处理逻辑
。
optimize
的过程就是深度遍历AST树
,先标记静态节点
,再标记静态根
。
单步调试代码
// src/main.js
import Vue from 'vue'
new Vue({
el: '#app',
template: `
<div>
<ul :class="bindCls" class="list" v-if="isShow">
<li v-for="(item, index) in data" :key="index" @click="clickItem(index)">
{{item}}:<{{index}}
</li>
</ul>
<div>
<p>111</p>
</div>
<p>222</p>
</div>`,
data() {
return {
bindCls: 'a',
isShow: true,
data: ['A', 'B', 'C', 'D']
}
},
methods: {
clickItem(index) {
console.log(index)
}
}
})
当我们的模板 template
经过 parse
过程后,会输出生成AST 树
,那么接下来我们需要对这颗树做优化,optimize
的逻辑是远简单于 parse
的逻辑,所以理解起来会轻松很多。
为什么要有优化过程,因为我们知道 Vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的
,也有很多数据是首次渲染后就永远不会变化的
,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch的过程跳过对他们的比对
。
来看一下 optimize
方法的定义,在 src/compiler/optimizer.js
中:
/**
* Goal of the optimizer: walk the generated template AST tree
* and detect sub-trees that are purely static, i.e. parts of
* the DOM that never needs to change.
*
* Once we detect these sub-trees, we can:
*
* 1. Hoist them into constants, so that we no longer need to
* create fresh nodes for them on each re-render;
* 2. Completely skip them in the patching process.
*/
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
markStatic(root)
// second pass: mark static roots.
markStaticRoots(root, false)
}
function genStaticKeys (keys: string): Function {
return makeMap(
'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +
(keys ? ',' + keys : '')
)
}
我们在编译阶段
可以把一些 AST 节点优化成静态节点
,所以整个 optimize
的过程实际上就干 2 件事情,
markStatic(root)
标记静态节点 ,markStaticRoots(root, false)
标记静态根。
标记静态节点
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
AST 元素节点
总共有 3 种类型,
- type 为 1 表示是
普通元素
, - type 为 2 表示是
表达式
, - type 为 3 表示是
纯文本
。
首先执行 node.static = isStatic(node)
isStatic
是对一个 AST 元素节点是否是静态的判断
,
- 如果是
表达式
,就是非静态
; - 如果是
纯文本
,就是静态
;
对于一个普通元素
,如果有pre 属性
,那么它使用了 v-pre
指令,是静态
,
否则要同时满足
以下条件:
- 没有使用
v-if
、v-for
, - 没有使用其它指令(不包括
v-once
), - 非内置组件,是平台保留的标签,
- 非带有
v-for
的template
标签的直接子节点, - 节点的所有属性的
key
都满足静态 key;
这些都满足则这个 AST 节点
是一个静态节点
。
如果这个节点是一个普通元素
,则遍历它的所有 children
,递归执行 markStatic
。
因为所有的 elseif
和 else
节点都不在 children
中, 如果节点的 ifConditions
不为空,则遍历 ifConditions
拿到所有条件中的 block
,也就是它们对应的AST 节点
,递归执行 markStatic
。
在这些递归过程中,一旦子节点有不是static
的情况,则它的父节点的 static
均变成 false
。
标记静态根
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor
}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
markStaticRoots
第二个参数是 isInFor
,对于已经是 static
的节点或者是 v-once
指令的节点,node.staticInFor = isInFor
。
接着就是对于 staticRoot
的判断逻辑,从注释中我们可以看到,对于有资格成为 staticRoot
的节点,除了本身是一个静态节点外,必须满足拥有 children
,并且 children
不能只是一个文本节点,不然的话把它标记成静态根节点的收益就很小了。
接下来和标记静态节点
的逻辑一样,遍历 children
以及 ifConditions
,递归执行 markStaticRoots
。
回归我们之前的例子,经过 optimize
后,AST 树变成了如下:
ast = {
'type': 1,
'tag': 'ul',
'attrsList': [],
'attrsMap': {
':class': 'bindCls',
'class': 'list',
'v-if': 'isShow'
},
'if': 'isShow',
'ifConditions': [{
'exp': 'isShow',
'block': // ul ast element
}],
'parent': undefined,
'plain': false,
'staticClass': 'list',
'classBinding': 'bindCls',
'static': false,
'staticRoot': false,
'children': [{
'type': 1,
'tag': 'li',
'attrsList': [{
'name': '@click',
'value': 'clickItem(index)'
}],
'attrsMap': {
'@click': 'clickItem(index)',
'v-for': '(item,index) in data'
},
'parent': // ul ast element
'plain': false,
'events': {
'click': {
'value': 'clickItem(index)'
}
},
'hasBindings': true,
'for': 'data',
'alias': 'item',
'iterator1': 'index',
'static': false,
'staticRoot': false,
'children': [
'type': 2,
'expression': '_s(item)+":"+_s(index)'
'text': '{{item}}:{{index}}',
'tokens': [
{'@binding':'item'},
':',
{'@binding':'index'}
],
'static': false
]
}]
}
我们发现每一个 AST 元素节点
都多了 staic属性
,并且 type为 1 的普通元素 AST 节点
多了 staticRoot
属性。
总结
那么至此我们分析完了 optimize
的过程,就是深度遍历这个 AST 树
,去检测它的每一颗子树是不是静态节点
如果是静态节点则它们生成 DOM 永远不需要改变
,这对运行时对模板的更新起到极大的优化
作用。
我们通过 optimize
我们把整个 AST 树中的每一个 AST 元素节点标记了 static
和 staticRoot
,它会影响我们接下来执行代码生成
的过程。
Vue源码学习目录
组件化 (一) createComponent
组件化 (二) patch
组件化 (三) 合并配置
组件化 (四) 生命周期
组件化(五) 组件注册
深入响应式原理(一) 响应式对象
深入响应式原理 (二)依赖收集 & 派发更新
深入响应式原理 (三)nextTick & 检测变化的注意事项
深入响应式原理 (四)计算属性 VS 侦听属性
深入响应式原理 (五)深入响应式原理 (五)组件更新
深入响应式原理 (六)Props (v2.6.11)
深入响应式原理 (七)原理图总结
编译(一)编译入口
编译(二)parse 解析模板字符串生成 AST
编译(三)optimize 优化AST树
编译(四)codegen 把优化后的 AST 树转换成可执行的代码
谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强