vue源码分析(十三) 编译之optimize
# 1. 概述
我们在 vue源码分析(八) 编译之整体流程 (opens new window) 中分析过了编译的三个过程,即解析模板字符串生成 AST
、优化语法树、生成代码,这一章我们主要来讲解第二部分——优化 AST
树。
为什么要优化 AST
树,我们先看一下官方的解释,如下:
源码目录: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.
*/
2
3
4
5
6
7
8
9
10
11
我们把这段注释翻译成中文,如下:
优化器的目标 遍历生成的模板AST树,检测纯静态的子树,即永远不需要更改的DOM。 一旦我们检测到这些子树,我们可以: 1、把它们变成常数,这样我们就不需要了在每次重新渲染时为它们创建新的节点 2、在修补过程中完全跳过它们。
从注释我们大致知道,optimize
的作用,我们知道 vue
是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM
也不会变化,我们可以在 patch
的过程跳过对他们的比对,提高性能。
所以 optimize
主要进行静态标注。下面我们从源码的角度分析,什么是静态标注?如何进行静态标注?
# 2. optimize
首先我们看一下 optimize
定义,如下:
源码目录:src/compiler/optimizer.js
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)
}
2
3
4
5
6
7
8
9
在分析之前,我们还是以一个案例进行讲解,如下:
<ul :class="classObject" class="list" v-show="isShow">
<li v-for="(l, i) in list" :key="i" ref="i" @click="clickItem(index)">{{ i }}:{{ l }}</li>
</ul>
2
3
上面的模板,通过上一章的parse函数的解析生成的ast树为,如下:
{
attrsList: [{name: "v-show", value: "isShow", start: 38, end: 53}],
attrsMap: {:class: "classObject", class: "list", v-show: "isShow"},
children: [
{
alias: "l",
attrsList: [{name: "@click", value: "clickItem(index)", start: 111, end: 136}],
attrsMap: {v-for: "(l, i) in list", :key: "i", ref: "i", @click: "clickItem(index)"},
children: [{
end: 152
expression: "_s(i)+":"+_s(l)",
start: 137,
text: "{{ i }}:{{ l }}",
tokens: (3) [ {@binding: "i"}, ":", {@binding: "l"}],
type: 2
}],
end: 157,
events: {click: {value: "clickItem(index)", dynamic: false, start: 111, end: 136}},
for: "list",
forProcessed: true,
hasBindings: true,
iterator1: "i",
key: "i",
parent: {/*省略 ul*/},
plain: false,
pre: undefined,
rawAttrsMap: {v-for: {…}, :key: {…}, ref: {…}, @click: {…}},
ref: ""i"",
refInFor: true,
start: 67,
tag: "li",
type: 1
}
],
classBinding: "classObject",
directives: [{name: "show", rawName: "v-show", value: "isShow", arg: null, isDynamicArg: false, modifiers: undefined, start: 38, end: 53}],
end: 173,
hasBindings: true,
parent: undefined,
plain: false,
rawAttrsMap: {
:class: {name: ":class", value: "classObject", start: 4, end: 24}
class: {name: "class", value: "list", start: 25, end: 37}
v-show: {name: "v-show", value: "isShow", start: 38, end: 53}
},
start: 0,
staticClass: ""list"",
tag: "ul",
type: 1
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
首先判断 root
即 AST
树存不存在,如果不存在直接返回,如果存在接下来调用 genStaticKeysCached
缓存了所有静态标签,赋值给 isStaticKey
。
接下获取判断是否是保留的标签的函数,赋值给变量 isPlatformReservedTag
。
说明:关于 genStaticKeysCached
和 isReservedTag
我们在后面小节中单独分析。
我们继续往下看,接下来是调用 markStatic(root)
标记所有非静态节点和调用 markStaticRoots(root, false)
标记静态 root
节点。
说明:关于markStatic
和 markStaticRoots
我们在后面小节中单独分析。
# 2.1 isReservedTag
isReservedTag
是从 options
配置中获取的,在 **vue源码分析(八) 编译之整体流程 (opens new window)**中我们分析过 baseOptions
的源码,知道 isReservedTag
的作用是检查给定的标签是否是保留的标签, 源码如下:
源码目录:src/platforms/web/util/element.js
export const isReservedTag = (tag: string): ?boolean => {
return isHTMLTag(tag) || isSVG(tag)
}
2
3
其中 isHTMLTag
和 isSVG
的定义如下:
源码目录:src/platforms/web/util/element.js
// html 保留标签
export const isHTMLTag = makeMap(
'html,body,base,head,link,meta,style,title,' +
'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
'embed,object,param,source,canvas,script,noscript,del,ins,' +
'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
'output,progress,select,textarea,' +
'details,dialog,menu,menuitem,summary,' +
'content,element,shadow,template,blockquote,iframe,tfoot'
)
// this map is intentionally selective, only covering SVG elements that may
// contain child elements.
// svg保留标签
export const isSVG = makeMap(
'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' +
'foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' +
'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',
true
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这里只要知道 isHTMLTag
和 isSVG
通过 makeMap
产生的, makeMap
的作用是根据 str
, 生成一个map
, 然后返回一个方法, 这个方法的作用是, 判断一个值是否在这个生成的 map
中。
# 2.2 makeMap
上面分析我们知道了 makeMap
的作用,接下来我们看一下 makeMap
的定义,如下:
源码目录:src/shared/util.js
export function makeMap (
str: string,
expectsLowerCase?: boolean
): (key: string) => true | void {
// 创建一个空对象
const map = Object.create(null)
// 通过 `,` 将字符串分割成数组,例如:[type,tag,attrsList,attrsMap,plain]
const list: Array<string> = str.split(',')
// 通过for循环将数组中的每一项作为对象的健,`true`作为值,给对象添加相应的属性
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
// 返回一个检查传递进来的参数是否在此对象中的函数
return expectsLowerCase
? val => map[val.toLowerCase()]
: val => map[val]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
最终返回的值一个函数,如下:
function (val) { return map[val.toLowerCase()]; }
# 2.3 genStaticKeysCached
我们来看看 genStaticKeysCached
的实现,源码如下:
源码目录:src/compiler/optimizer.js
const genStaticKeysCached = cached(genStaticKeys)
genStaticKeysCached
通过我们前面分析过的 cached
函数来实现,参数是 genStaticKeys
函数,我们先看一下 genStaticKeys
的定义,再来回顾一下 cached
的实现,源码如下:
源码目录:src/compiler/optimizer.js
function genStaticKeys (keys: string): Function {
return makeMap(
'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
(keys ? ',' + keys : '')
)
}
2
3
4
5
6
前面我们已经分析过了 makeMap
的作用。在这里上面的字符串中 keys
为 staticClass,staticStyle
,所以最后的字符串为 type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,staticClass,staticStyle
,然后通过 makeMap
生成一个闭包,如:
function (val) {
var map = {
type: ture,
tag: ture,
attrsList: ture,
attrsMap: ture,
plain: ture,
parent: ture,
children: ture,
attrs: ture,
start: ture,
end: ture,
rawAttrsMap: ture,
staticClass: ture,
staticStyle: ture
}
return map[val.toLowerCase()];
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2.4 cached
我们先来看一下 cached
的定义如下:
源码目录:src/shared/util.js
export function cached<F: Function> (fn: F): F {
// 创建一个空对象
const cache = Object.create(null)
return (function cachedFn (str: string) {
// 获取缓存对象str属性的值
const hit = cache[str]
// 如果该值存在,直接返回,不存在调用一次fn,然后将结果存放到缓存对象中
return hit || (cache[str] = fn(str))
}: any)
}
2
3
4
5
6
7
8
9
10
cached
方法接受一个参数为函数,其会将该传入 fn
函数的运行结果缓存,返回一个函数 cachedFn
,函数内部先获取调用该函数 cachedFn
传入的参数 str
在缓存对象 cache
中的的值,如果在缓存对象 cache
中有值,直接返回该值,否则调用 cached
函数中传入的方法 fn
,然后将运行结果存到 cache
中。这样如果 cachedFn
调用两次,第一次要执行一次 fn
,并将其运行结果缓存起来,当第二次调用 cachedFn
并且参数 str
与之前调用过的一致的时候,直接从缓存对象 cache
中获取结果,这样就不用再一次调用 fn
方法,节省一次函数的运行。
通过上面的分析,我们知道在编译阶段可以把一些 AST
节点优化成静态节点,所以整个 optimize
的过程实际上就干 2 件事情,markStatic(root)
标记静态节点 ,markStaticRoots(root, false)
标记静态根。
接下来我们分析这两件事执行的代码逻辑。
# 3. markStatic
我们来看一下markStatic
的定义,如下:
源码目录:src/compiler/optimizer.js
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
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
从上面的代码我们可以看出,markStatic
函数的主要作用是标注节点的状态和对标签节点进行处理,下面我们就详细分析一下这两个过程。
# 3.1 标注节点的状态
首先看标记静态节点的 markStatic
这个方法,如下:
源码目录:src/compiler/optimizer.js
node.static = isStatic(node)
markStatic
一开始通过调用 isStatic
进行标注节点的状态,下面我们来分析isStatic
,源码如下:
源码目录:src/compiler/optimizer.js
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)
))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
isStatic
函数主要的作用是:
- 判断节点类型为表达式,标注为非静态。
- 判断节点类型为普通文本,标注为静态。
v-pre
指令(无需编译)标注为静态,或者满足以下条件,也标注为静态。- (1) 无动态绑定;
- (2) 没有
v-if
和v-for
; - (3) 不是内置的标签,内置的标签有
slot
和component
; - (4) 是平台保留标签(
html
和svg
标签); - (5) 不是
template
标签的直接子元素并且没有包含在for
循环中; - (6) 结点包含的属性只能有
isStaticKey
中指定的几个;
# 3.2 对标签节点进行处理
分析完 isStatic
我们再回到 markStatic
,继续往下看,接下的代码是对标签节点进行处理。
首先判断节点类型为1即普通元素,则执行 if
语句中的代码。
说明:AST
元素节点总共有 3
种类型, type
为 1
表示是普通元素,为2
表示是表达式,为 3
表示是纯文本。
接下来又是一个 if
语句,成立的条件是既不是平台保留标签(html,svg
)也不是slot
标签又不是一个内联模板容器,此段代码的作用是对 slot
内容不做递归标注,直接返回。
继续往下看,紧接着是一个 for
循环子节点,进行递归标注,如果子节点为非静态,那么该节点也要标注非静态。所以整个标注过程是自下而上,先标注子节点,然后再是父节点,一层一层往上回溯。
最后因为所有的 elseif
和 else
节点都不在 children
中, 如果节点的 ifConditions
不为空,则遍历 ifConditions
拿到所有条件中的 block
,也就是它们对应的 AST
节点,递归执行 markStatic
。在这些递归过程中,一旦子节点有不是 static
的情况,则它的父节点的static
均变成 false
。
通过调用 markStatic
函数处理过的 ast
树如下:
{
attrsList: [{name: "v-show", value: "isShow", start: 38, end: 53}],
attrsMap: {:class: "classObject", class: "list", v-show: "isShow"},
children: [
{
alias: "l",
attrsList: [{name: "@click", value: "clickItem(index)", start: 111, end: 136}],
attrsMap: {v-for: "(l, i) in list", :key: "i", ref: "i", @click: "clickItem(index)"},
children: [{
end: 152
expression: "_s(i)+":"+_s(l)",
start: 137,
static: false,
text: "{{ i }}:{{ l }}",
tokens: (3) [ {@binding: "i"}, ":", {@binding: "l"}],
type: 2
}],
end: 157,
events: {click: {value: "clickItem(index)", dynamic: false, start: 111, end: 136}},
for: "list",
forProcessed: true,
hasBindings: true,
iterator1: "i",
key: "i",
parent: {/*省略 ul*/},
plain: false,
pre: undefined,
rawAttrsMap: {v-for: {…}, :key: {…}, ref: {…}, @click: {…}},
ref: ""i"",
refInFor: true,
start: 67,
static: false,
tag: "li",
type: 1
}
],
classBinding: "classObject",
directives: [{name: "show", rawName: "v-show", value: "isShow", arg: null, isDynamicArg: false, modifiers: undefined, start: 38, end: 53}],
end: 173,
hasBindings: true,
parent: undefined,
plain: false,
rawAttrsMap: {
:class: {name: ":class", value: "classObject", start: 4, end: 24}
class: {name: "class", value: "list", start: 25, end: 37}
v-show: {name: "v-show", value: "isShow", start: 38, end: 53}
},
start: 0,
static: false,
staticClass: ""list"",
tag: "ul",
type: 1
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
我们发现每一个 AST
元素节点都多了 staic
属性。
# 4. markStaticRoots
我们继续看 markStaticRoots
函数的定义,如下:
源码目录:src/compiler/optimizer.js
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)
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
markStaticRoots
第二个参数是 isInFor
,对于已经是 static
的节点或者是 v-once
指令的节点,node.staticInFor = isInFor
。 接着就是对于 staticRoot
的判断逻辑,从注释中我们可以看到,对于有资格成为 staticRoot
的节点,除了本身是一个静态节点外,必须满足拥有 children
,并且 children
不能只是一个文本节点,不然的话把它标记成静态根节点的收益就很小了。
接下来和标记静态节点的逻辑一样,遍历 children
以及 ifConditions
,递归执行 markStaticRoots
。
通过调用 markStaticRoots
函数处理过的 ast
树如下:
{
attrsList: [{name: "v-show", value: "isShow", start: 38, end: 53}],
attrsMap: {:class: "classObject", class: "list", v-show: "isShow"},
children: [
{
alias: "l",
attrsList: [{name: "@click", value: "clickItem(index)", start: 111, end: 136}],
attrsMap: {v-for: "(l, i) in list", :key: "i", ref: "i", @click: "clickItem(index)"},
children: [{
end: 152
expression: "_s(i)+":"+_s(l)",
start: 137,
static: false,
text: "{{ i }}:{{ l }}",
tokens: (3) [ {@binding: "i"}, ":", {@binding: "l"}],
type: 2
}],
end: 157,
events: {click: {value: "clickItem(index)", dynamic: false, start: 111, end: 136}},
for: "list",
forProcessed: true,
hasBindings: true,
iterator1: "i",
key: "i",
parent: {/*省略 ul*/},
plain: false,
pre: undefined,
rawAttrsMap: {v-for: {…}, :key: {…}, ref: {…}, @click: {…}},
ref: ""i"",
refInFor: true,
start: 67,
static: false,
staticRoot: false,
tag: "li",
type: 1
}
],
classBinding: "classObject",
directives: [{name: "show", rawName: "v-show", value: "isShow", arg: null, isDynamicArg: false, modifiers: undefined, start: 38, end: 53}],
end: 173,
hasBindings: true,
parent: undefined,
plain: false,
rawAttrsMap: {
:class: {name: ":class", value: "classObject", start: 4, end: 24}
class: {name: "class", value: "list", start: 25, end: 37}
v-show: {name: "v-show", value: "isShow", start: 38, end: 53}
},
start: 0,
static: false,
staticClass: ""list"",
staticRoot: false,
tag: "ul",
type: 1
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
我们发现 type
为 1
的普通元素 AST
节点多了 staticRoot
属性。
# 5. 总结
至此我们分析完了 optimize
的过程,就是深度遍历这个 AST
树,去检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成 DOM
永远不需要改变,这对运行时对模板的更新起到极大的优化作用。
我们通过 optimize
我们把整个 AST
树中的每一个 AST
元素节点标记了 static
和 staticRoot
,它会影响我们接下来执行代码生成的过程。