将模板转化成供 Vue 实例在挂载时可调用的 render 函数。分为三个节点

  • 模板解析阶段
  • 优化阶段
  • 代码生成阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export const createCompiler = createCompilerCreator(function baseCompile(
template,
options
) {
// 模板解析阶段: 用正则等方式解析 template 模板中的指令、class、style等数据,形成AST
const ast = parse(template.trim(), options);
if (options.optimize !== false) {
// 优化阶段: 遍历AST,找出其中的静态节点,并打上标记
optimize(ast, options);
}
// 代码生成阶段: 将AST转换成渲染函数
const code = generate(ast, options);
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns,
};
});

模板解析

将模板字符串用正则等方式解析成 AST

parseHTML

html 解析器,解析模板字符串

4 个钩子函数: start()、end()、chars()、comment()

解析不同的内容

  • 解析 HTML 注释,例如 <!-- 我是注释 -->: 正则匹配 <!-- 开头和 --> 结尾的
  • 解析条件注释,例如 <!-- [if !IE]> -->我是注释<!--< ![endif] -->: 正则匹配 <![ 开头和 ]> 结尾的
  • 解析 DOCTYPE,例如 <!DOCTYPE html>: 同解析条件注释
  • 解析开始标签: 调用 parseStartTag 函数,完成后调用 handleStartTag 生成 AST
    • 解析标签属性
    • 解析标签是否是自闭合
  • 解析结束标签: 如果模板字符串符合结束标签的特征,则会获得匹配结果数组;如果不合符,则得到 null
  • 解析文本
    • 匹配 < 出现的位置
    • 如果第一个 < 在第一个位置,那么说明模板字符串是以其它 5 种类型开始的;
    • 如果第一个 < 不在第一个位置而在模板字符串中间某个位置,那么说明模板字符串是以文本开头的,那么从开头到第一个 < 出现的位置就都是文本内容了;
    • 如果在整个模板字符串里没有找到 <,那说明整个模板字符串都是文本

保证 AST 节点层级关系

  • 开始标签入栈,结束标签出栈,若没有正确闭合,则抛出警告: tag has no matching end tag.
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
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
export function parse(template, options) {
// ...
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
// 当解析到开始标签时,调用该函数,标签名tag、标签属性attrs、标签是否自闭合unary
start(tag, attrs, unary) {
let element = createASTElement(tag, attrs, currentParent);
},
// 当解析到结束标签时,调用该函数
end() {},
// 当解析到文本时,调用该函数
// parseText: 判断传入的文本是否包含变量;构造expression;构造tokens
chars(text) {
if ((res = parseText(text))) {
// 动态文本类型 AST 节点
let element = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text,
};
} else {
// 静态文本类型 AST 节点
let element = {
type: 3,
text,
};
}
},
// 当解析到注释时,调用该函数
comment(text) {
let element = {
type: 3,
text,
isComment: true,
};
},
});
return root;
}

export function createASTElement(tag, attrs, parent) {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
parent,
children: [],
};
}

export function parseHTML(html, options) {
var stack = []; // 维护AST节点层级的栈
var expectHTML = options.expectHTML;
var isUnaryTag$$1 = options.isUnaryTag || no; //用来检测一个标签是否是可以省略闭合标签的非自闭合标签
var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
var index = 0; //解析游标,标识当前从何处开始解析模板字符串
var last, // 存储剩余还未解析的模板字符串
lastTag; // 存储着位于 stack 栈顶的元素

// 开启一个 while 循环,循环结束的条件是 html 为空,即 html 被 parse 完毕
while (html) {
last = html;
// 确保即将 parse 的内容不是在纯文本标签里 (script,style,textarea)
// lastTag为栈顶元素,!lastTag即表示当前html字符串没有父节点,而isPlainTextElement(lastTag) 是检测 lastTag 是否为是那三个纯文本标签之一,是的话返回true,不是返回fasle
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf("<");
/**
* 如果html字符串是以'<'开头,则有以下几种可能
* 开始标签:<div>
* 结束标签:</div>
* 注释:<!-- 我是注释 -->
* 条件注释:<!-- [if !IE] --> <!-- [endif] -->
* DOCTYPE:<!DOCTYPE html>
* 需要一一去匹配尝试
*/
if (textEnd === 0) {
// 解析是否是注释
if (comment.test(html)) {
}
// 解析是否是条件注释
if (conditionalComment.test(html)) {
}
// 解析是否是DOCTYPE
const doctypeMatch = html.match(doctype);
if (doctypeMatch) {
}
// 解析是否是结束标签
const endTagMatch = html.match(endTag);
if (endTagMatch) {
}
// 匹配是否是开始标签
const startTagMatch = parseStartTag();
if (startTagMatch) {
}
}
// 如果html字符串不是以'<'开头,则解析文本类型
let text, rest, next;
if (textEnd >= 0) {
}
// 如果在html字符串中没有找到'<',表示这一段html字符串都是纯文本
if (textEnd < 0) {
text = html;
html = "";
}
// 把截取出来的text转化成textAST
if (options.chars && text) {
options.chars(text);
}
} else {
// 父元素为script、style、textarea时,其内部的内容全部当做纯文本处理
}

//将整个字符串作为文本对待
if (html === last) {
options.chars && options.chars(html);
if (!stack.length && options.warn) {
options.warn('Mal-formatted tag at end of template: "' + html + '"');
}
break;
}
}

// html === last,即html字符串中的标签格式有误时会跳出while循环,调用
// 于处理栈中剩余未处理的标签,此时parseEndTag函数里的pos为0,那么pos>=0就会恒成立,那么就会逐个警告缺少闭合标签,并调用 options.end将其闭合
parseEndTag();
//parse 开始标签
function parseStartTag() {}
//处理 parseStartTag 的结果
function handleStartTag(match) {}
//parse 结束标签
function parseEndTag(tagName, start, end) {}
}

// 解析html注释
(function () {
const comment = /^<!\--/;
if (comment.test(html)) {
// 若为注释,则继续查找是否存在'-->'
const commentEnd = html.indexOf("-->");

if (commentEnd >= 0) {
// 若存在 '-->',继续判断options中是否保留注释,可以在<template></template>标签上配置comments选项,对应options.shouldKeepComment
if (options.shouldKeepComment) {
// 若保留注释,则把注释截取出来传给options.comment,创建注释类型的AST节点
options.comment(html.substring(4, commentEnd));
}
// 若不保留注释,则将游标移动到'-->'之后,继续向后解析
advance(commentEnd + 3);
// continue
}
}
})(
// 解析条件注释
function () {
const conditionalComment = /^<!\[/;
if (conditionalComment.test(html)) {
// 若为条件注释,则继续查找是否存在']>'
const conditionalEnd = html.indexOf("]>");

if (conditionalEnd >= 0) {
// 若存在 ']>',则从原本的html字符串中把条件注释截掉,
// 把剩下的内容重新赋给html,继续向后匹配
advance(conditionalEnd + 2);
// continue
}
}
}
)(
// 解析doc
function () {
const doctype = /^<!DOCTYPE [^>]+>/i;
// 解析是否是DOCTYPE
const doctypeMatch = html.match(doctype);
if (doctypeMatch) {
advance(doctypeMatch[0].length);
// continue
}
}
)(
// 解析开始标签
function () {
const ncname = "[a-zA-Z_][\\w\\-\\.]*";
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`);
const startTagClose = /^\s*(\/?)>/;
const attribute =
/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

function parseStartTag() {
const start = html.match(startTagOpen);
// '<div></div>'.match(startTagOpen) => ['<div','div',index:0,input:'<div></div>']
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index,
};
advance(start[0].length);
let end, attr;
/** 解析标签属性
* <div a=1 b=2 c=3></div>
* 从<div之后到开始标签的结束符号'>'之前,一直匹配属性attrs
* 所有属性匹配完之后,html字符串还剩下
* 自闭合标签剩下: '/>'
* 非自闭合标签剩下: '></div>'
*/
while (
!(end = html.match(startTagClose)) &&
(attr = html.match(attribute))
) {
advance(attr[0].length);
match.attrs.push(attr);
}

/** 解析标签是否是自闭合
* 这里判断了该标签是否为自闭合标签
* 自闭合标签如:<input type='text' />
* 非自闭合标签如:<div></div>
* '></div>'.match(startTagClose) => [">", "", index: 0, input: "></div>", groups: undefined]
* '/><div></div>'.match(startTagClose) => ["/>", "/", index: 0, input: "/><div></div>", groups: undefined]
* 因此,我们可以通过end[1]是否是"/"来判断该标签是否是自闭合标签
*/
if (end) {
match.unarySlash = end[1];
advance(end[0].length);
match.end = index;
return match;
}
}
}

function handleStartTag(match) {
const tagName = match.tagName; // 开始标签的标签名
const unarySlash = match.unarySlash; // 是否为自闭合标签的标志,自闭合为"",非自闭合为"/"

if (expectHTML) {
// ...
}

const unary = isUnaryTag(tagName) || !!unarySlash; // 布尔值,标志是否为自闭合标签

const l = match.attrs.length; // match.attrs 数组的长度
const attrs = new Array(l); // 一个与match.attrs数组长度相等的数组
for (let i = 0; i < l; i++) {
const args = match.attrs[i];
const value = args[3] || args[4] || args[5] || "";

// 如果 shouldDecodeNewlines 为 true,意味着 Vue 在编译模板的时候,要对属性值中的换行符或制表符做兼容处理。
// 而shouldDecodeNewlinesForHref为 true 意味着Vue在编译模板的时候,要对a标签的 href属性值中的换行符或制表符做兼容处理。
const shouldDecodeNewlines =
tagName === "a" && args[1] === "href"
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines;

attrs[i] = {
name: args[1], // 标签属性的属性名,如class
value: decodeAttr(value, shouldDecodeNewlines), // 标签属性的属性值,如class对应的a
};
}

// 非自闭合标签
if (!unary) {
stack.push({
tag: tagName,
lowerCasedTag: tagName.toLowerCase(),
attrs: attrs,
});
lastTag = tagName;
}

// 自闭合标签
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end);
}
}
}
)(
// 解析结束标签
function () {
const ncname = "[a-zA-Z_][\\w\\-\\.]*";
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
const endTagMatch = html.match(endTag);

"</div>".match(endTag); // ["</div>", "div", index: 0, input: "</div>", groups: undefined]
"<div>".match(endTag); // null

if (endTagMatch) {
const curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);
// continue
}

// 三个可选参数
// 第一种是三个参数都传递,用于处理普通的结束标签
// 第二种是只传递tagName
// 第三种是三个参数都不传递,用于处理栈中剩余未处理的标签
function parseEndTag(tagName, start, end) {
let pos, lowerCasedTagName;
if (start == null) start = index;
if (end == null) end = index;

if (tagName) {
lowerCasedTagName = tagName.toLowerCase();
}

// 如果tagName存在,那么就从后往前遍历栈,在栈中寻找与tagName相同的标签并记录其所在的位置pos,如果tagName不存在,则将pos置为0
if (tagName) {
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break;
}
}
} else {
pos = 0;
}

if (pos >= 0) {
// Close all the open elements, up the stack
for (let i = stack.length - 1; i >= pos; i--) {
// 如果发现stack栈中存在索引大于pos的元素,那么该元素一定是缺少闭合标签的
// 这是因为在正常情况下,stack栈的栈顶元素应该和当前的结束标签tagName 匹配,也就是说正常的pos应该是栈顶位置,后面不应该再有元素,如果后面还有元素,那么后面的元素就都缺少闭合标签
if (
process.env.NODE_ENV !== "production" &&
(i > pos || !tagName) &&
options.warn
) {
options.warn(`tag <${stack[i].tag}> has no matching end tag.`);
}
// 立即将其闭合,为了保证解析结果的正确性
if (options.end) {
options.end(stack[i].tag, start, end);
}
}

// 把pos位置以后的元素都从stack栈中弹出,以及把lastTag更新为栈顶元素
stack.length = pos;
lastTag = pos && stack[pos - 1].tag;
}
// 单独判断 br 或p标签
// 浏览器会自动把</br>标签解析为正常的 <br>标签,而对于</p>浏览器则自动将其补全为<p></p>,所以Vue为了与浏览器对这两个标签的行为保持一致,故对这两个便签单独判断处理
else if (lowerCasedTagName === "br") {
if (options.start) {
options.start(tagName, [], true, start, end);
}
} else if (lowerCasedTagName === "p") {
if (options.start) {
options.start(tagName, [], false, start, end);
}
if (options.end) {
options.end(tagName, start, end);
}
}
}
}
)(
// 解析文本
function () {
let textEnd = html.indexOf("<");
// '<' 在第一个位置,为其余5种类型
if (textEnd === 0) {
// ...
}
// '<' 不在第一个位置,文本开头
if (textEnd >= 0) {
// 如果html字符串不是以'<'开头,说明'<'前面的都是纯文本,无需处理
// 那就把'<'以后的内容拿出来赋给rest
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
/**
* 用'<'以后的内容rest去匹配endTag、startTagOpen、comment、conditionalComment
* 如果都匹配不上,表示'<'是属于文本本身的内容
*/
// 在'<'之后查找是否还有'<'
next = rest.indexOf("<", 1);
// 如果没有了,表示'<'后面也是文本
if (next < 0) break;
// 如果还有,表示'<'是文本中的一个字符
textEnd += next;
// 那就把next之后的内容截出来继续下一轮循环匹配
rest = html.slice(textEnd);
}
// '<'是结束标签的开始 ,说明从开始到'<'都是文本,截取出来
text = html.substring(0, textEnd);
advance(textEnd);
}
// 整个模板字符串里没有找到`<`,说明整个模板字符串都是文本
if (textEnd < 0) {
text = html;
html = "";
}
// 把截取出来的text转化成textAST
if (options.chars && text) {
options.chars(text);
}
}
);

parseText

文本解析器,解析静态和动态文本

1
2
3
4
5
6
7
8
9
10
11
12
let text = "我叫{{name}},我今年{{age}}岁了"
let res = parseText(text)
res = {
expression:"我叫"+_s(name)+",我今年"+_s(age)+"岁了",
tokens:[
"我叫",
{'@binding': name },
",我今年"
{'@binding': age },
"岁了"
]
}
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
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g;
const buildRegex = cached((delimiters) => {
const open = delimiters[0].replace(regexEscapeRE, "\\$&");
const close = delimiters[1].replace(regexEscapeRE, "\\$&");
return new RegExp(open + "((?:.|\\n)+?)" + close, "g");
});

export function parseText(text, delimiters) {
// 一个正则表达式,用来检查文本中是否包含变量
// delimiters 默认为 {{}},也可以自定义变量包裹符,比如 %name%
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
if (!tagRE.test(text)) {
return;
}
const tokens = [];
const rawTokens = [];
/**
* let lastIndex = tagRE.lastIndex = 0
* 上面这行代码等同于下面这两行代码:
* tagRE.lastIndex = 0
* let lastIndex = tagRE.lastIndex
*/
let lastIndex = (tagRE.lastIndex = 0);
let 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
// 取出'{{ }}'中间的变量exp
const exp = parseFilters(match[1].trim());
// 把变量exp改成_s(exp)形式也放入tokens中
tokens.push(`_s(${exp})`);
rawTokens.push({ "@binding": exp });
// 设置lastIndex 以保证下一轮循环时,只从'}}'后面再开始匹配正则
lastIndex = index + match[0].length;
}
// 当剩下的text不再被正则匹配上时,表示所有变量已经处理完毕
// 此时如果lastIndex < text.length,表示在最后一个变量后面还有文本
// 最后将后面的文本再加入到tokens中
if (lastIndex < text.length) {
rawTokens.push((tokenValue = text.slice(lastIndex)));
tokens.push(JSON.stringify(tokenValue));
}

// 最后把数组tokens中的所有元素用'+'拼接起来
return {
expression: tokens.join("+"),
tokens: rawTokens,
};
}

优化

遍历 AST,找出其中的静态节点,并打上标记,从而在 patch 过程中,diff 算法直接跳过静态节点,减少比较的过程

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
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
export function optimize(root, options) {
if (!root) return;
isStaticKey = genStaticKeysCached(options.staticKeys || "");
isPlatformReservedTag = options.isReservedTag || no;
// 标记静态节点
markStatic(root);
// 标记静态根节点
markStaticRoots(root, false);
}

// 标记静态节点
function markStatic(node) {
node.static = isStatic(node);
if (node.type === 1) {
// 元素节点递归遍历子节点
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;
}
}
// 不在 children 中的子节点,比如被 v-if 隐藏了
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) {
if (node.type === 2) {
// 包含变量的动态文本节点
return false;
}
if (node.type === 3) {
// 不包含变量的纯文本节点
return true;
}
// 元素节点判断
return !!(
node.pre || // 使用了v-pre指令,则是静态节点
(!node.hasBindings && // 不能使用动态绑定语法,即标签上不能有v-、@、:开头的属性
!node.if &&
!node.for && // 不能使用v-if、v-else、v-for指令
!isBuiltInTag(node.tag) && // 不能是内置组件,即标签名不能是slot和component
isPlatformReservedTag(node.tag) && // 标签名必须是平台保留标签,即不能是组件
!isDirectChildOfTemplateFor(node) && // 当前节点的父节点不能是带有 v-for 的 template 标签
Object.keys(node).every(isStaticKey)) // 节点的所有属性的 key 都必须是静态节点才有的 key,注: 静态节点的key是有限的,它只能是type,tag,attrsList,attrsMap,plain,parent,children,attrs之一
);
}

// 标记静态根节点
function markStaticRoots(node, isInFor) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor;
}
// 为了使节点有资格作为静态根节点,它应具有不只是静态文本的子节点。 否则,优化的成本将超过收益,最好始终将其更新。
// 1. 节点本身必须是静态节点
// 2. 必须拥有子节点 children
// 3. 子节点不能只是只有一个文本节点
if (
node.static &&
node.children.length &&
!(node.children.length === 1 && node.children[0].type === 3)
) {
node.staticRoot = true;
return;
} else {
node.staticRoot = false;
}

// 如果当前节点不是静态根节点,那就继续递归遍历它的子节点node.children和node.ifConditions
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);
}
}
}
}

代码生成

将 AST 转换成渲染函数,生成 render 函数字符串

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
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
export function generate(ast, option) {
const state = new CodegenState(options);
const code = ast ? genElement(ast, state) : '_c("div")';
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns,
};
}

// 最终生成: 元素节点,文本节点,注释节点
export function genElement(el, state) {
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) {
return genChildren(el, state) || "void 0";
} else if (el.tag === "slot") {
return genSlot(el, state);
} else {
// component or element
let code;
if (el.component) {
code = genComponent(el.component, el, state);
} else {
const data = el.plain ? undefined : genData(el, state);
const children = el.inlineTemplate ? null : genChildren(el, state, true);
code = `_c('${el.tag}'${
data ? `,${data}` : "" // data
}${
children ? `,${children}` : "" // children
})`;
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code);
}
return code;
}
}

// 生成元素节点 _c
const data = el.plain ? undefined : genData(el, state);

const children = el.inlineTemplate ? null : genChildren(el, state, true);
// 节点的标签名tagName,节点属性data,节点的子节点列表children
code = `_c('${el.tag}'${
data ? `,${data}` : "" // data
}${
children ? `,${children}` : "" // children
})`;

// 获取节点属性data
// 逻辑: 在拼接字符串,先给data赋值为一个{,然后判断存在哪些属性数据,就将这些数据拼接到data中,最后再加一个},最终得到节点全部属性data
export function genData(el, state) {
let data = "{";
const 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,`;
}
// 自定义事件
if (el.events) {
data += `${genHandlers(el.events, false, state.warn)},`;
}
// 浏览器原生事件
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true, state.warn)},`;
}
// 篇幅所限,省略其他情况的判断
data = data.replace(/,$/, "") + "}";
return data;
}

// 获取子节点列表children
// 遍历AST的children属性中的元素,然后根据元素属性的不同生成不同的VNode创建函数调用字符串
export function genChildren(el) {
if (children.length) {
return `[${children.map((c) => genNode(c, state)).join(",")}]`;
}
}
function genNode(node, state) {
if (node.type === 1) {
return genElement(node, state);
}
if (node.type === 3 && node.isComment) {
return genComment(node);
} else {
return genText(node);
}
}

// 生成文本节点 _v
export function genText(text) {
return `_v(${
text.type === 2
? text.expression // no need for () because already wrapped in _s()
: transformSpecialNewlines(JSON.stringify(text.text))
})`;
}

// 生成注释节点 _e
export function genComment(comment) {
return `_e(${JSON.stringify(comment.text)})`;
}

流程总结

  1. 挂载调用全局实例方法 $mount
  2. 最开始调用的 compileToFunctions 函数内部调用了 compile 函数
  3. 在 compile 函数内部又调用了 baseCompile 函数
  4. 而 baseCompile 函数返回的是代码生成阶段生成好的 render 函数字符串,在 compileToFunctions 函数内部调用 compile 函数就可以拿到生成好的 render 函数字符串
  5. 在 compileToFunction s 函数内部将 render 函数字符串传给 createFunction 函数从而变成真正的 render 函数返回出去
  6. 最后将其赋值给 options.render
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
69
70
71
72
73
74
75
76
Vue.prototype.$mount = function (el) {
const options = this.$options;
// 如果用户没有手写render函数
if (!options.render) {
// 获取模板,先尝试获取内部模板,如果获取不到则获取外部模板
let template = options.template;
if (template) {
} else {
template = getOuterHTML(el);
}
const { render, staticRenderFns } = compileToFunctions(
template,
{
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments,
},
this
);
options.render = render;
options.staticRenderFns = staticRenderFns;
}
};

const { render, staticRenderFns } = compileToFunctions(
template,
{
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments,
},
this
);

const { compile, compileToFunctions } = createCompiler(baseOptions);

export function createCompilerCreator(baseCompile) {
return function createCompiler(baseOptions) {};
}

function createCompiler(baseOptions) {
function compile(template, options) {
const compiled = baseCompile(template, finalOptions);
compiled.errors = errors;
compiled.tips = tips;
return compiled;
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile),
};
}

export function createCompileToFunctionFn(compile) {
return function compileToFunctions() {
// compile
const res = {};
const compiled = compile(template, options);
res.render = createFunction(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map((code) => {
return createFunction(code, fnGenErrors);
});
return res;
};
}

function createFunction(code, errors) {
try {
return new Function(code);
} catch (err) {
errors.push({ err, code });
return noop;
}
}