Vue源码解析6-静态AST优化

上篇文章说到了模板解析的第一步parse,现在来说第二步optimize,用于优化静态内容的渲染,主要是给静态节点打上一些标记。

Vue中对于生成的AST会做优化,静态内容是指和数据没有关系,不需要每次都刷新的内容,这一步主要就是找出 ast 中的静态内容,并加以标注。

这一步的代码比parse要少太多,应该压力会小很多 🙄,先看一下入口代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 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: (key)=> boolean,判断一个key是否是
// type,tag,attrsList,attrsMap,plain,parent,children,attrs其中之一
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);
}

可以看到代码非常简短,只有两步:markStaticmarkStaticRoots。我们挨个把这里的每个子函数讲一下。

genStaticKeysCached、isStaticKey

genStaticKeysCached用于缓存一个函数的执行结果,这种技巧在很多地方有可以用到,比如求解斐波那契数列。

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
const genStaticKeysCached = cached(genStaticKeys); // 缓存genStaticKeys结果,每次先从缓存中查找,找不到再执行genStaticKeys

/**
* Create a cached version of a pure function.
*/
export function cached<F: Function>(fn: F): F {
const cache = Object.create(null);
return (function cachedFn(str: string) {
const hit = cache[str];
return hit || (cache[str] = fn(str)); // 缓存中有就直接返回,没有的话再执行求解函数
}: any);
}

function genStaticKeys(keys: string): Function {
return makeMap('type,tag,attrsList,attrsMap,plain,parent,children,attrs' + (keys ? ',' + keys : ''));
}

/**
* Make a map and return a function for checking if a key
* is in that map.
*/
export function makeMap(str: string, expectsLowerCase?: boolean): (key: string) => true | void {
const map = Object.create(null);
const list: Array<string> = str.split(',');
for (let i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase ? val => map[val.toLowerCase()] : val => map[val];
}

所以经过一系列嵌套,我们的isStaticKey就是查找指定key是否存在于makeMap的结果中,如果之前已经查找过这个key那么可以直接从cache中拿到缓存的结果。

markStatic

这个函数会遍历整个 AST,为了更好的了解其中的过程,最好进行断点调试每一步,比如如下的template

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
这里是文本<箭头之后的文本
<p>{{message}}</p>
<p>静态文本<a href="https://www.baidu.com">博客地址</a></p>
</div>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
message: '动态文本',
},
});
</script>

生成的AST会是:

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,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [{
type: 3,
text: '这里是文本<箭头之后的文本'
},
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
parent: ,
children: [{
type: 2,
expression: '_s(message)',
text: '{{message}}'
}],
plain: true
},
{
text: " ",
type: 3
},
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
children: [{
text: "静态文本",
type: 3
},
{
attrs: [{name: "href", value: '"http://www.baidu.com"'}],
attrsList: [{name: "href", value: 'http://www.baidu.com'}],
attrsMap: {href: 'http://www.baidu.com'}
children: [{
text: "博客地址",
type: 3
}]
plain: false,
tag: "a",
type: 1
}
],
plain: true
}
],
plain: false,
attrs: [{name: "id", value: "'app'"}]
}

现在看看函数定义,它的目的是给 AST 上每个节点打上static标记。

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
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;
}
// 若有其中一个child的static为false,则父节点的static也需要设置为false
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i];
markStatic(child);
if (!child.static) {
node.static = false;
}
}
// 若节点属于v-if、v-else-if、v-else,则只要其中一个分支不是static的,整个node就设置为不是static
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;
}
}
}
}
}

思路是先利用isStatic判断自身是否是static的,然后判断所有children,只要其中一个child不是static的,那么自己也不是static的;最后如果处于v-if、v-else-if、v-else,则只要其中一个分支不是static的,整个node就设置为不是static

isStatic

判断一个 AST 节点是否为静态的,上面也提到静态内容是指和数据没有关系,不需要每次都刷新的内容。

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
function isStatic(node: ASTNode): boolean {
if (node.type === 2) {
// expression
return false;
}
if (node.type === 3) {
// text
return true;
}
return !!(
node.pre || // 节点上有v-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))
);
}

// 是否位于一个<template v-for="xxx">
function isDirectChildOfTemplateFor(node: ASTElement): boolean {
while (node.parent) {
node = node.parent;
if (node.tag !== 'template') {
return false;
}
if (node.for) {
return true;
}
}
return false;
}

可以看到标记static的条件有 2 个:

  1. 节点上有v-pre指令,官网文档也说了在编译时遇到这个指令会跳过它
  2. 没有动态绑定属性 && 不是v-if && 不是v-for && 不是内置的标签slotcomponent && 是平台保留标签,即HTMLSVG标签 && 不是位于一个<template v-for="xxx"> && 节点的所有属性均是type,tag,attrsList,attrsMap,plain,parent,children,attrs其中之一。

第二个条件非常严格,想成为static的还真是不容易。。。

经过这一步之后,我们的AST变为

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
56
57
58
59
60
61
62
63
64
{
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [{
type: 3,
text: '这里是文本<箭头之后的文本',
static: true
},
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
parent: ,
children: [{
type: 2,
expression: '_s(message)',
text: '{{message}}',
static: false
}],
plain: true,
static: false
},
{
text: " ",
type: 3,
static: true
},
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
children: [{
text: "静态文本",
type: 3,
static: true
},
{
attrs: [{name: "href", value: '"http://www.baidu.com"'}],
attrsList: [{name: "href", value: 'http://www.baidu.com'}],
attrsMap: {href: 'http://www.baidu.com'}
children: [{
text: "博客地址",
type: 3,
static: true
}],
plain: false,
tag: "a",
type: 1,
static: true
}
],
plain: true,
static: true
}
],
plain: false,
attrs: [{name: "id", value: "'app'"}],
static: false
}

markStaticRoots

用来找到那种本身是static的,同时只有唯一的一个text子节点,将他们标记为staticRoot,即静态根节点。

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
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本身是static的,同时只有唯一的一个text子节点
node.staticRoot = true;
return;
} else {
node.staticRoot = false;
}
// 递归children,为它们标记staticRoot
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for); // 注意这里比下面的递归多了一个node.for
}
}
// 若节点属于v-if、v-else-if、v-else,遍历所有分支
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor);
}
}
}
}

最终咱们optimize过的AST如下:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
{
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [{
type: 3,
text: '这里是文本<箭头之后的文本',
static: true
},
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
parent: ,
children: [{
type: 2,
expression: '_s(message)',
text: '{{message}}',
static: false
}],
plain: true,
static: false,
staticRoot: false
},
{
text: " ",
type: 3,
static: true
},
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
children: [{
text: "静态文本",
type: 3,
static: true
},
{
attrs: [{name: "href", value: '"http://www.baidu.com"'}],
attrsList: [{name: "href", value: 'http://www.baidu.com'}],
attrsMap: {href: 'http://www.baidu.com'}
children: [{
text: "博客地址",
type: 3,
static: true
}],
plain: false,
tag: "a",
type: 1,
static: true
}
],
plain: true,
static: true,
staticInFor: false,
staticRoot: true
}
],
plain: false,
attrs: [{name: "id", value: "'app'"}],
static: false,
staticRoot: false
}