# 模板编译原理
vue提供了模板语法,允许我们声明式地描述状态和
DOM
之间的绑定关系,然后通过模板来生成真实DOM
并将其呈现在用户界面上。在底层实现上,vue会将模板编译成虚拟DOM
渲染函数。当应用内部的状态发生变化时,vue可以结合响应式系统,聪明地找出最小数量的组件进行重新渲染以及最少量地进行DOM
操作。
在vue中创建Html
并不是只有模板这一种途径,我们既可以手动写渲染函数来创建,也可以使用JSX
来创建。渲染函数是创建HTML
最原始的方法。模板最终会通过编译转换成渲染函数,渲染函数执行后,会得到一份vnode
用于虚拟DOM
渲染。所以模板编译其实是配合虚拟DOM
进行渲染。
渲染函数的作用是每次执行它,它就会使用当前最新的状态生成一份新的vnode
,然后使用这个vnode
进行渲染。
# 将模板编译成渲染函数
先将模板解析成
AST
(抽象语法树),然后使用抽象语法树生成渲染函数。
TIP
由于静态节点不需要总是重新渲染,所以生成AST
之后,生成渲染函数之前这个阶段,需要做一个操作,那就是遍历一遍抽象语法树,给所有静态节点做一个标记,这样在虚拟DOM
中更新节点时,如果发现这个节点有标记就不会重新渲染它。
对于 Vue
组件来说,模板编译只会在组件实例化的时候编译一次,生成渲染函数之后在也不会进行编译。因此,编译对组件的 runtime
是一种性能损耗。而模板编译的目的仅仅是将template
转化为render function
,这个过程,正好可以在项目构建的过程中完成,这样可以让实际组件在 runtime
时直接跳过模板渲染,进而提升性能,这个在项目构建的编译template
的过程,就是预编译。
所以模板编译大体分为三部分:
- 将模板解析为
AST
--解析器 - 遍历抽象语法树标记静态节点--优化器
- 使用抽象语法树生成渲染函数--代码生成器
# 解析器
在解析器内部分成很多小解析器:包括过滤器解析器,文本解析器和HTML
解析器,然后通过一条主线将这些解析器组装在一起。
- 过滤器解析器
用来解析过滤器的
- 文本解析器
用来解析带变量的文本
HTML
解析器
解析模板,每当解析到
HTML
标签的开始位置,结束位置,文本或者注释,都会触发钩子函数,然后将相关信息通过参数传递出来。
TIP
主线上做的事情就是监听HTML
解析器。每当触发钩子函数时,就生成一个对应的抽象语法树节点。生成抽象语法树前,会根据类型使用不同的方式生成不同抽象语法树。例如,如果是文本节点,就生成文本类型的AST
。当解析器不再触发钩子函数时,就说明所有模板都解析完毕,所有类型的节点都在钩子函数中构建完成,即AST
构建完成。
# 解析器内部原理
钩子函数
start
有三个参数,分别是tag/attrs/unary
。分别说明标签名,标签的属性和是否是自闭合标签。而文本节点的钩子函数chars
和注释节点的钩子函数comment
都只有一个参数,只有text
。这是因为构建元素节点时需要知道标签名,属性和自闭合标识,而构建注释节点和文本节点只需要知道文本即可。
<div>
<h1>
我是林嘉恒
</h1>
<p>
今年24岁
</p>
</div>
2
3
4
5
6
7
8
function createASTElement ( // 构造元素类型的AST节点
tag,
attrs,
parent
) {
return {
type: 1,
tag: tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent: parent,
children: []
}
}
parseHTML(template, {
start(tag, attrs, unary, start$1, end) {
var element = createASTElement(tag, attrs, currentParent); // 在start钩子函数创建一个元素类型的ast节点
}
chars (text, start, end) { // 触发文本的钩子函数
let element = { type: 3, text }
}
comment (text, start, end) { // 触发注释
let element = { type: 3, text, isComment: true}
}
})
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
TIP
这个模板被解析成AST
过程需要注意的点:
- 利用一个栈来记录层级关系
- 基于
HTML
解析器的逻辑,我们可以在每次触发钩子函数start
时,把当前构建的节点推入栈中;每当触发钩子函数end
时就会从栈中弹出一个节点。 - 这样就可以保证每当触发钩子函数
start
时,栈中最后一个节点就是当前正在构建的节点的父节点。 - 模板中开始位置的一些空格,会触发文本节点的钩子函数,在钩子函数里会忽略这些空格。同时会在模板中将这些空格截取掉。
- 解析到文本节点时,栈中最后一个节点就是其父节点,将文本节点添加到该父节点的子节点中。由于文本节点没有子节点所以文本节点不会被推入到栈中。
- 栈空时,就得到一个完整的带层级关系的抽象语法树。
- 自闭合标签不需要入栈
# HTML
解析器 运行原理
解析HTML
模板的过程就是循环的过程,简单来说就是用HTML
模板字符串来循环,每轮循环都从HTML
模板中截取一小段字符串,然后重复上述过程,直到HTML
模板被截成一个空字符串时循环结束。
解析器如何知道每轮循环应该截取哪些字符串的?
截取类型:
- 开始标签:
<div>
- 结束标签:
</div>
HTML
注释:例如<!-- 我是注释 -->
DOCTYPE
:例如<!DOCTYPE html>
- 条件注释:例如
<!-- [if !IE] --> 我是注释 <!-- ![endif]>
- 文本
- 模版截取,只有以开始标签开头的才需要进行标签截取操作,先判断是否以
<
开头 - 判断是
<
开头后,还需要通过正则来确定具体标签类型 - 触发
start
钩子函数需要几个参数,所以需要将标签名,属性以及自闭合标识解析出来 - 进一步解析属性和自闭合标识。每解析一个属性就截取一个属性,截取完剩下的
html
模版依然符合标签属性的正则,说明还有剩余属性需要处理。重复执行 - 截取结束标签跟开始标签一样,判断是否
<
开头,然后进一步判断剩余HTML
模版的开始位置是否符合正则表达式即可。 - 如果
HTML
模板中第一个字符不是<
就一定是文本,只要找到下一个<
位置,就可以截取到文本 - 如果将
<
前面的字符截取完之后,剩余的模板不符合任何需要被解析的片段类型,说明<
是文本一部分。当判断出是属于文本一部分后,就要找到下一个<
,并将其前面的文本截取出来并加到之前截取一板的文本后面 - 如何知道父元素
lastTag
是谁呢,通过栈,该栈还有另外一个作用可以检测出标签是否正确闭合
var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
var ncname = "[a-zA-Z_][\\-\\.0-9_a-zA-Z" + (unicodeRegExp.source) + "]*";
var qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
var startTagOpen = new RegExp(("^<" + qnameCapture));
var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
var startTagClose = /^\s*(\/?)>/;
var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
var comment = /^<!\--/;
var doctype = /^<!DOCTYPE [^>]+>/i;
// 以开始标签开始的模版
'<div></div>'.match(startTagOpen) // ['<div', 'div', groups: undefined, index: 0, input: "<div></div>"]
// 以结束标签开始的模版
'</div>'.match(endTag) // ["</div>", 'div', index: 0, input: '</div>']
var textEnd = html.indexOf('<');
if (textEnd === 0) {
// 注释
if (comment.test(html)) {
var commentEnd = html.indexOf('-->');
if (commentEnd >= 0) {
if (options.shouldKeepComment) { // 在完整构建版本中的浏览器内编译时可用。保留注释
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);
}
advance(commentEnd + 3);
continue
}
}
// 条件注释
if (conditionalComment.test(html)) {
var conditionalEnd = html.indexOf(']>');
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2);
continue
}
}
// Doctype:
var doctypeMatch = html.match(doctype);
if (doctypeMatch) {
advance(doctypeMatch[0].length);
continue
}
// End tag:
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length); // 模板截取
parseEndTag(endTagMatch[1], curIndex, index);
continue
}
// Start tag:
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1);
}
continue // 这一轮解析工作已经完成,进行下一轮解析工作
}
}
// 截取文本
var text = (void 0), rest = (void 0), next = (void 0);
if (textEnd >= 0) {
rest = html.slice(textEnd);
while ( // 这里是为了防止 < 是文本的一部分,
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// < in plain text, be forgiving and treat it as text
next = rest.indexOf('<', 1);
if (next < 0) { break }
textEnd += next;
rest = html.slice(textEnd);
}
text = html.substring(0, textEnd);
}
if (textEnd < 0) {
text = html; // 如果模板找不到<,就说明整个模板都是文本
}
if (text) {
advance(text.length); // 直接将html设置为空了相当于
}
if (options.chars && text) {
options.chars(text, index - text.length, index);
}
function advance (n) {
index += n;
html = html.substring(n);
}
function parseStartTag () {
var start = html.match(startTagOpen);
if (start) {
var match = {
tagName: start[1],
attrs: [],
start: index
};
advance(start[0].length);
var end, attr;
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
// 这里的意思:如果剩余html模版不符合开始标签结尾部分的特征,并且符合标签属性的特征,就进入循环中解析和截取操作
attr.start = index;
advance(attr[0].length);
attr.end = index;
match.attrs.push(attr);
// 解析后结果:保存在attrs中
// {
// tagName: 'div',
// attrs: [
// [' class="box"', 'class', '=', 'box', null, null],
// [' id="el"', 'id', '=', 'el', null, null]
// ]
// }
// 解析后:剩余 ></div>
// 解析前:' class="box" id="el"></div>
}
if (end) {
match.unarySlash = end[1]; // 自闭合标签解析后的该属性为/,非自闭合标签为空字符串
advance(end[0].length);
match.end = index;
return match
}
}
}
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
纯文本内容元素处理?script/style/textarea
元素叫做纯文本内容元素。解析他们的时候会把这三种标签内容当文本处理
- 把文本截取出来并触发钩子函数
chars
,然后再将结束标签截取出来并触发钩子函数end
var last, lastTag; // lastTag表示父元素
while(html) {
last = html;
if (!lastTag || !isPlainTextElement(lastTag)) {
// 前面分析的内容
} else {
// 纯文本内容处理逻辑
var endTagLength = 0;
var stackedTag = lastTag.toLowerCase();
var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length;
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1);
}
if (options.chars) {
options.chars(text);
}
return ''
});
index += html.length - rest$1.length;
html = rest$1;
parseEndTag(stackedTag, index - endTagLength, index);
}
}
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
# 文本解析器
文本解析器的作用是解析文本。奇怪的是文本不是已经在HTML
解析器中被解析出来了么?准确来说文本解析器是对HTML
解析器解析出来的文本进行二次加工。因为文本分两种类型,一种是纯文本,另一种是带变量的文本。
在vue中我们可以使用变量来填充模板。而HTML
解析器在解析文本时,并不会区分文本是否带变量的文本。如果是纯文本不需要进行任何处理;但如果是带变量的文本,那么需要使用文本解析器进一步解析。因为带变量的文本在使用虚拟DOM
进行渲染时,需要将变量替换成变量的值。
在文本解析器中,第一步做的事情就是使用正则表达式来判断文本是否为带变量的文本,也就是检查文本中是否包含这样的语法。如果是纯文本,则直接返回
undefined
;如果是带变量的文本,再进行二次加工。
function parseText (
text,
delimiters
) {
// defaultTagRE: /\{\{((?:.|\r?\n)+?)\}\}/g; delimiters 只在完整构建版本浏览器内编译使用,改变纯文本插入分隔符
var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
if (!tagRE.test(text)) { // 纯文本直接返回undefined了
return
}
var tokens = [];
var rawTokens = [];
var lastIndex = tagRE.lastIndex = 0;
var match, index, tokenValue;
while ((match = tagRE.exec(text))) {
index = match.index;
// push text token
if (index > lastIndex) { // 先把 {{ 前面的文本添加到tokens中
rawTokens.push(tokenValue = text.slice(lastIndex, index));
tokens.push(JSON.stringify(tokenValue));
}
// tag token
var exp = parseFilters(match[1].trim());
tokens.push(("_s(" + exp + ")"));
rawTokens.push({ '@binding': exp });
lastIndex = index + match[0].length; // 设置lastIndex来保证下一轮循环时,正则表达式不再重复匹配已经解析过的文本
}
if (lastIndex < text.length) { // 当所有变量都处理完毕后,如果最后一个变量右边还有文本,就将文本添加到数组中
rawTokens.push(tokenValue = text.slice(lastIndex));
tokens.push(JSON.stringify(tokenValue));
}
return {
expression: tokens.join('+'), // 将数组里面的文本合成一个字符串
tokens: rawTokens
}
}
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
每当HTML
解析器解析到文本时触发chars
函数,并且解析得到文本。在chars
函数中需要构建文本类型的AST
,并将它添加到父节点的children
属性中。如果是带变量的文本需要经过parseText
二次加工。此时构建了一个带变量的文本类型AST
并加到父节点children
属性中。
如何解决带变量的文本呢:使用正则表达式匹配出文本中的变量,先把变量左边的文本添加到数组中,然后把变量改成
_s(x)
这样的形式也添加到数组中。如果变量后面有变量则重复这动作。这时有一个数组,数组元素和文本顺序一致,此时将数组元素用+
连起来变成字符串,就可以得到最后结果。
"Hello {{name}}"
// 解析后expression:
'Hello ' + _s(name)
// _s(x)就是toString别名
export function toString (val: any): string {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}
2
3
4
5
6
7
8
9
10
11
12
# 优化器
优化器的目标遍历AST
,检测出所有静态子树(永远都不会发生变化的DOM
节点)并打上标记。当AST
中静态子树被打上标记后,每次重新渲染时就不需要为打上标记的静态节点创建新的虚拟节点,而是直接克隆已经存在的虚拟节点。静态子树是指的是那些在AST
中永远都不会发生变化的节点
- 标记静态子树有两点好处:
- 每次重新渲染时,不需要为静态子树创建新节点;
- 在虚拟
DOM
中打补丁patching
的过程可以跳过。
TIP
每次重新渲染都会使用最新的状态生成一份全新的VNode
与旧的VNode
进行对比。而生成VNode
的过程中,如果发现一个节点被标记为静态子树,那么除了首次渲染会生成节点之外,在重新渲染时并不会生成新的子节点树,而是克隆已存在的静态子树。
打补丁patch
过程中,如果两个节点都是静态子树,就不需要进行对比与更新DOM
的操作,直接跳过。因为静态子树是不可变的,不需要对比就知道不可能发生变化。
- 优化器内部实现两个步骤:
- 在
AST
中找出所有静态节点并打上标记 - 在
AST
中找出所有静态根节点并打上标记
- 在
- 落实到
AST
中静态节点指的是static
属性为true
的值 - 如果一个节点下面所有子节点都是静态节点,并且父级是动态节点,那么它就是静态根节点。
staticRoot
为true
function optimize (root, options) {
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
# 找出所有静态节点并标记
递归处理 root
根节点,先判断根节点是不是静态根节点,再用相同方式处理子节点。
function isDirectChildOfTemplateFor (node) {
while (node.parent) {
node = node.parent;
if (node.tag !== 'template') {
return false
}
if (node.for) {
return true
}
}
return false
}
function isStatic (node) {
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)
))
}
function markStatic (node) {
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 (var i = 0, l = node.children.length; i < l; i++) {
var child = node.children[i];
markStatic(child);
if (!child.static) { // 做校对
node.static = false;
}
}
if (node.ifConditions) {
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
var block = node.ifConditions[i$1].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
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
node.pre
:如果元素节点使用了指令v-pre
,可以直接断定是一个静态节点- 不能使用
v-if/v-for/v-else
指令 - 不能是内置标签,也就是说标签名不能是
slot/component
- 不能是组件
- 当前节点的父节点不能是带有
v-for
的template
标签 - 节点中不存在动态节点才会有的属性
- 不能使用动态绑定语法,也就是说标签上不能有以
v-/@/:
开头的属性。不包括v-for/v-if/v-else/v-else-if/v-once
'节点不存在动态节点才会有的属性'这里的意思是如果一个元素节点是静态节点,那么这个节点上的属性其实有范围。范围是
type/tag/attrsList/attrsMap/plain/parent/children/attrs/staticClass/staticStyle
。 递归从上到下依次标记,如果父节点被标记为静态节点后,子节点却为动态,这样就矛盾了。因为静态子树所有子节点都为静态节点。所以需要校验一下
# 找出所有静态根节点并标记
function markStaticRoots (node, isInFor) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor;
}
// 要使节点符合静态根节点的要求,必须有子节点
// 这个子节点不能只有一个静态文本的子节点,否则优化成本超出效益
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 (var i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for);
}
}
if (node.ifConditions) {
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
markStaticRoots(node.ifConditions[i$1].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
- 跟找出静态节点过程类似。不一样的是如果一个节点被判断为静态根节点,不会继续向子级继续寻找。
- 优化成本大于收益的,即使是静态根节点也不会标记
# 代码生成器
代码生成器是模板函数编译最后一步,作用是将AST
转换成渲染函数中的内容,这个内容可以称为代码字符串。
<p title='Berwin' @click='c'>1</p>
// 生成的代码字符串
`with(this){return _c('p',{attrs: {"titile":"Berwin"}, on: {"clcik":c}}, [_v("1")]}`
//格式化后
with(this) {
return _c(
'p',
{
attrs:{"title":"Berwin"},
on:{"click":c}
},
[_v("1")]
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TIP
渲染函数之所以可以生成vnode
,是因为代码字符串中会有很多函数调用(如_c/_v
),这些函数是虚拟DOM
提供创建vnode
的方法。vnode
有很多种类型,不同的类型对应不同的创建方法,所以代码字符串中的_c/_v
其实都是创建vnode
的方法,只是创建的vnode
的类型不同。例如,_c
可以创建元素类型的vnode
,而_v
可以创建文本类型的vnode
。
代码字符串中的_c
其实是createElement
的别名。createElement
是虚拟DOM
中提供的方法,作用是创建虚拟节点,有三个参数:
- 标签名
- 一个包含模板相关属性的数据对象
- 子节点列表
这也知道了渲染函数可以生成
VNode
的原因:渲染函数其实是执行了createElement
,而createElement
可以创建一个VNode
代码生成器其实就是字符串拼接的过程。通过递归
AST
来生成字符串,最先生成根节点,然后在子节点字符串生成后,将其拼接在根节点的参数中,子节点的子节点拼接在子节点的参数中,这样一层一层地拼接,直到最后拼接成完整的字符串。会将字符串在with
中返回给调用者。这样一个代码字符串最终导出到外界使用时,会将代码字符串放到函数里,这个函数叫做渲染函数
三种节点对应的创建方法与别名
类型 | 创建方法 | 别名 |
---|---|---|
元素节点 | createElement | _c |
文本节点 | createTextNode | _v |
注释节点 | createEmptyNode | _e |
# 代码生成器原理
- 生成元素节点,其实就是生成一个
_c
的函数调用字符串
function genElement (el, state) {
if (el.parent) {
el.pre = el.pre || el.parent.pre;
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
var code;
if (el.component) {
code = genComponent(el.component, el, state);
} else {
var data;
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData$2(el, state);
}
var children = el.inlineTemplate ? null : genChildren(el, state, true);
code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
}
// module transforms
for (var i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code);
}
return code
}
}
function genData$2 (el, state) {
var data = '{';
// directives first.
// directives may mutate the el's other properties before they are generated.
var dirs = genDirectives(el, state);
if (dirs) { data += dirs + ','; }
// key
if (el.key) {
data += "key:" + (el.key) + ",";
}
// ref
if (el.ref) {
data += "ref:" + (el.ref) + ",";
}
if (el.refInFor) {
data += "refInFor:true,";
}
// pre
if (el.pre) {
data += "pre:true,";
}
// record original tag name for components using "is" attribute
if (el.component) {
data += "tag:\"" + (el.tag) + "\",";
}
// module data generation functions
for (var i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el);
}
// attributes
if (el.attrs) {
data += "attrs:" + (genProps(el.attrs)) + ",";
}
// DOM props
if (el.props) {
data += "domProps:" + (genProps(el.props)) + ",";
}
// event handlers
if (el.events) {
data += (genHandlers(el.events, false)) + ",";
}
if (el.nativeEvents) {
data += (genHandlers(el.nativeEvents, true)) + ",";
}
// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += "slot:" + (el.slotTarget) + ",";
}
// scoped slots
if (el.scopedSlots) {
data += (genScopedSlots(el, el.scopedSlots, state)) + ",";
}
// component v-model
if (el.model) {
data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
}
// inline-template
if (el.inlineTemplate) {
var inlineTemplate = genInlineTemplate(el, state);
if (inlineTemplate) {
data += inlineTemplate + ",";
}
}
data = data.replace(/,$/, '') + '}';
// v-bind dynamic argument wrap
// v-bind with dynamic arguments must be applied using the same v-bind object
// merge helper so that class/style/mustUseProp attrs are handled correctly.
if (el.dynamicAttrs) {
data = "_b(" + data + ",\"" + (el.tag) + "\"," + (genProps(el.dynamicAttrs)) + ")";
}
// v-bind data wrap
if (el.wrapData) {
data = el.wrapData(data);
}
// v-on data wrap
if (el.wrapListeners) {
data = el.wrapListeners(data);
}
return data
}
function genChildren (
el,
state,
checkSkip,
altGenElement,
altGenNode
) {
var children = el.children;
if (children.length) {
var el$1 = children[0];
// optimize single v-for
if (children.length === 1 &&
el$1.for &&
el$1.tag !== 'template' &&
el$1.tag !== 'slot'
) {
var normalizationType = checkSkip
? state.maybeComponent(el$1) ? ",1" : ",0"
: "";
return ("" + ((altGenElement || genElement)(el$1, state)) + normalizationType)
}
var normalizationType$1 = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0;
var gen = altGenNode || genNode;
return ("[" + (children.map(function (c) { return gen(c, state); }).join(',')) + "]" + (normalizationType$1 ? ("," + normalizationType$1) : ''))
}
}
function genNode (node, state) {
if (node.type === 1) {
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
- 代码中
el
的plain
属性是在编译时发现的,如果节点没有属性,就会把plain
设置为true
。 - 代码中主要逻辑是用
genData
和genChildren
分别获取data
和children
,然后将它们分别拼到字符串中指定位置,最后把拼好的_c(tagName, data, children)
返回 data
也是字符串,先给data
赋值一个{
,然后发现节点存在哪些属性数据,将这些数据拼接到data
中,最后拼接一个}
- 生成
children
字符串逻辑也是拼接字符串。通过循环子节点列表,根据不同子节点类型生成不同节点字符串并将其拼接到一起。
- 生成文本节点:把文本放在
_v
函数的参数中即可
function genText (text) {
return ("_v(" + (text.type === 2
? text.expression // no need for () because already wrapped in _s()
: transformSpecialNewlines(JSON.stringify(text.text))) + ")")
}
2
3
4
5
- 判断文本类型:如果动态文本则使用
expression
;如果静态则使用text
text
使用JSON.stringfy
原因是:可以给文本包装一层字符串
JSON.stringify('Hello') // "'Hello'"
- 注释节点:只需要把文本放在
_e
参数中即可
function genComment (comment) {
return ("_e(" + (JSON.stringify(comment.text)) + ")")
}
2
3
# 总结
vue中的模板template
无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML
语法,所有需要将template
转化成一个JavaScript
函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML
元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。
- 模板编译又分三个阶段,解析
parse
,优化optimize
,生成generate
,最终生成可执行函数render
。parse
阶段:使用大量的正则表达式对template
字符串进行解析,将标签、指令、属性等转化为抽象语法树AST
。optimize
阶段:遍历AST
,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff
比较时,直接跳过这一些静态节点,优化runtime
的性能。generate
阶段:将最终的AST
转化为render
函数字符串,将字符串拼在with
中返回给调用者
function generate (
ast,
options
) {
var state = new CodegenState(options);
// fix #11483, Root level <script> tags should not be rendered.
var code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}
2
3
4
5
6
7
8
9
10
11
12