Vue的模板编译原理及源码解析

一、什么是模板编译

模板编译有点类似于代码的编译器,主要用于将HTML解析成AST树,方便后续Vue对网页内容进行操作。

二、模板编译的架构

模板编译模块主要分为三个部分:parser(解析器)、optimizer(优化器)和code generator(代码生成器)

其中:
parser(解析器)用于解析HTML、模板变量和属性,最终生成AST树
optimizer(优化器)用于标记静态节点,被标记的静态节点不会参与重新渲染,达到优化性能的目的
code generator(代码生成器)会将AST树拼装成一段以“with(this)”开头的字符串,把它交给JS引擎执行就能生成对应的虚拟DOM

三、一切的开始

src/platforms/web/runtime-with-compiler.ts中调用了compileToFunctions进行模板编译,这是模板编译的入口。
关键代码如下:

1
2
3
4
5
6
7
8
9
// src/platforms/web/runtime-with-compiler.ts

const {render, staticRenderFns} = compileToFunctions(
template,
...
this
)
options.render = render
options.staticRenderFns = staticRenderFns

这里调用了函数compileToFunctions,其第一个参数是template,由options传入,也可以指定el,然后通过getOuterHTML来获得。
返回的参数是render跟staticRenderFns,render函数用于渲染虚拟节点,staticRenderFns用于生成静态节点。

而这个compileToFunctions定义在:

1
2
// src/platforms/web/compiler/index.ts
const {compile, compileToFunctions} = createCompiler(baseOptions)

可以看到compileToFunctions就是createCompiler的别名,其相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/compiler/index.ts
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)

return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})

在createCompiler中,先通过parse函数生成AST树,接着交给optimize进行优化,最后调用generate生成代码字符串。

四、解析器Parser

  1. 解析HTML
    在parse中,核心部分就是调用了parseHTML来解析HTML,然后通过向parseHTML传递回调函数start、end、chars和comment来处理相应解析的结果:
    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
    // src/compiler/parser/index.ts
    export function parse(....): ASTElement {
    ...
    let root
    parseHTML(template, {
    ...,
    // 处理解析开始标签返回的结果
    start(...) {
    ...
    },
    // 处理解析结束标签返回的结果
    end(...) {
    ...
    },
    // 处理解析文本内容返回的结果
    chars(...) {
    ...
    },
    // 处理注释返回的结果
    comment(...) {
    ...
    }
    })
    // 最终生成的AST
    return root
    }

而这个parseHTML是用了jQuery的作者Johb Resig写的开源库:htmlparser.js
在parseHTML中,通过while循环来解析html,接着通过正则表达式来匹配标签、属性跟文本,最后使用advance函数将已经处理完的内容裁剪掉,大致代码如下:

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
// src/compiler/parser/html-parser.ts
export function parseHTML(html, options: HTMLParserOptions) {
...
let last, lastTag
while(html) {
// 排除script和style标签
if(!lastTag || !isPlainTextElement(lastTag)) {
// 匹配开始标签的尖括号
let textEnd = html.indexOf('<')

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 curIdx = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIdx, index)
continue
}

// 匹配并处理开始标签
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
...
continue
}
}

// 匹配文本内容
let text, rest, next
if (textEnd >= 0) {
// 将textEnd之后的部分截取出来
rest = html.slice(textEnd)
// 这一段逻辑用来处理文本内容中出现尖括号的情况
while(
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// 第二个参数表示不匹配首字符
// 因为将尖括号前面的部分slice掉,第一个字符必然是尖括号,所以这里的尖括号会匹配到结束标签的尖括号
next = rest.indexOf('<', 1)
if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}

text = html.substring(0, textEnd)
}

// 如果匹配不到东西,说明这段html格式有问题,全部跳过
if (textEnd < 0) {
text = html
}

if (text) {
advance(text.length)
}

if (options.chars && text) {
options.chars(text, index - text.length, index)
}
}
// script/style标签单独处理
else {
...
}
}
}

其中裁剪函数advance的代码如下:

1
2
3
4
5
6
7
// src/compiler/parser/html-parser.ts
function advance(n) {
// index用于记录当前未处理的html字符位置
index += n
// 截取功能是用substring实现的,返回的是序号n之后的字符串
html = html.substring(n)
}

而parseHTML这个函数主要做三件事情:处理开始标签、处理结束标签以及标签间的文本内容

1)首先看看代码是如何处理开始标签的
在处理开始标签的时候,主要用到了两个函数parseStartTag和handleStartTag
先看看parseStartTag的代码实现:

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
// src/compiler/parser/html-parser.ts
function parseStartTag() {
// 通过正则表达式startTagOpen来匹配开始标签
const start = html.match(startTagOpen)
if (start) {
// 建立一个match对象,用于记录开始标签中的信息
const match: any = {
tagName: start[1],
attrs: [],
start: index
}

// 截取掉开始标签的开头部分,方便后续的属性匹配
advance(start[0].length)

let end, attr
while(
// 通过startTagClose判断这个标签是否是自闭合标签
!(end = html.match(startTagClose) &&
// 匹配动态标签和静态标签,动态标签就是Vue中v-开头的属性,如v-bind、v-model、v-on这些
(attr = html.match(dynamicArgAttribute) || attr = html.match(attribute))
) {
attr.start = index
advance(attr[0].length)
attr.end = index
// 将属性保存到match对象中的attrs属性中
match.attrs.push(attr)
}

if (end) {
// 如果该标签是自闭合标签,那么就新增一个unarySlash属性来表示它
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}

匹配标签各部分内容是通过各种正则表达式来实现的,如startTagOpen的正则表达式就是:

1
2
3
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)

这个看起来有点晕,先把它简化一下,ncname中的unicode属于特殊情况,一般不会出现,所以可以改成:

1
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`

接着qnameCapture中的冒号:属于xml中才会出现的格式,是一种很古老的格式,现在一般html不会用到。所以startTagOpen最终可以简化成:

1
const startTagOpen = new RegExp(`^<([a-zA-Z_][\\-\\.0-9_a-zA-Z]*)`)

最后我们可以借助正则表达式可视化工具:

这里可以看到,这段正则其实就是匹配一个尖括号以及后面的标签,比如一段html代码像这样:

1
<div class="main">123</div>

表达式匹配的就是

1
<div

这部分

当匹配成功之后,代码就会建立一个match对象用来保存标签中的信息。
接着匹配开始标签后面的部分,也就是属性部分。
这里代码通过while循环来逐个匹配属性,而这里的属性有两种形式:

一种是普通的静态属性,对应的正则表达式是:

1
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

其可视化如下:

可以看出来这段正则的匹配逻辑就是 “空格+属性名+空格+等号+空格+属性值” 这样的形式,其中属性值并没有严格的格式规定,有可能是双引号、单引号,或者干脆啥都不加,所以表达式通过“|”分成三种情况来处理。

另一种则是动态属性,也就是Vue新增的v-bind、v-model、v-if、v-on这一类属性,其对应的正则表达式如下:

1
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

通过可视化:

可以看出这个表达式其实就是在静态属性的基础上,增加了对属性名更细致化的匹配,会根据前缀v-、@、:、#来匹配动态属性。

最后是匹配开始标签的结束标志。
其正则表达式如下:

1
const startTagClose = /^\s*(\/?)>/

这一段正则表达式就很简单了,就是匹配多个空格加上一个或零个斜杠,再加上右尖括号
斜杠是用来匹配自闭合标签,就是下面这些:

1
<input /><br/>

如果匹配到了,那就在match中新增一个属性unarySlash,保存end[1],也就是说end[1]有东西,那就说明这个标签是个自闭合标签。
以上就是parseStartTag的核心逻辑,处理完parseStartTag之后,会将match返回给外部,交给handleStartTag来处理。

handleStartTag的大致代码如下:

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
// src/compiler/parser/html-parser.ts
function handleStartTag(match) {
const tagName = match.tagName
const unarySlash = match.unarySlash

...

// 判断是否是自闭合标签
const unary = isUnaryTag(tagName) || !!unarySlash

const l = match.attrs.length
const attrs: ASTAttr[] = new Array(l)
for (let i = 0; i < l; i ++) {
const args = match.attrs[i]
// 这里args345匹配的是双引号、单引号以及没有引号三种形式的属性值
const value = args[3] || args[4] || args[5] || ''
...
attrs[i] = {
name: args[1]
value: value
}
...
}

if (!unary) {
stack.push({
tag: tagName,
lowerCasedTag: tagName.toLowerCase(),
attrs: attrs,
start: match.start,
end: match.end
})
lastTag = tagName
}

if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}

从上面这段代码可以得知,handleStartTag主要做两件事情:

  • 将属性转换成ASTAttr对象,再压入stack中
  • 将结果返回给回调函数start
    转换ASTAttr对象这部分逻辑并不复杂,这里就不细说,而回调函数start会放到后面来讲。

2)接着看看文本内容的处理逻辑

处理文本内容的大致代码如下:

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
// src/compiler/parser/html-parser.ts
// 匹配文本内容
let text, rest, next
if (textEnd >= 0) {
// 将textEnd之后的部分截取出来
rest = html.slice(textEnd)
// 这一段逻辑用来处理文本内容中出现尖括号的情况
while(
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// 第二个参数表示不匹配首字符
// 因为将尖括号前面的部分slice掉,第一个字符必然是尖括号,所以这里的尖括号会匹配到结束标签的尖括号
next = rest.indexOf('<', 1)
if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}

text = html.substring(0, textEnd)
}

// 如果匹配不到东西,说明这段html格式有问题,全部跳过
if (textEnd < 0) {
text = html
}

if (text) {
advance(text.length)
}

if (options.chars && text) {
options.chars(text, index - text.length, index)
}

在这段代码中,如果是普通的文本内容,直接通过html.substring(0, textEnd)截取即可。因为textEnd匹配的左尖括号刚好就是结束标签的左尖括号。
但这里还有对一种特殊情况进行处理,那就是在文本内容中出现左尖括号,这会导致textEnd匹配的位置异常。
具体的处理方法是:
a.先通过html.slice(textEnd)来去掉左尖括号前面的部分,让左尖括号永远在字符的第一位
b.借助while循环判断当前这段html是否是开始标签、结束标签、注释、条件注释,如果是,那就跳出循环
c.如果不是,那就通过indexOf(‘<’, 1)匹配下一个左尖括号,用textEnd记录下这个左尖括号的位置,最后用slice(textEnd)将尖括号左边部分删掉
最后将得到的文本内容text交给回调函数chars处理即可。

3)最后看看结束标签的处理

匹配结束标签的正则表达式跟开始标签差不多,就是多加了一道斜杠:

1
const endTag = new RegExp(`^<\\/([a-zA-Z_][\\-\\.0-9_a-zA-Z]*)[^>]*>`)

接着将匹配到的标签名交给parseEndTag处理,大致代码:

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
// src/compiler/parser/html-parser.ts

function parseEndTag(tagName?: any, start?: any, end?: any) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index

if (tagName) {
// 通过出栈的方式从stack里面找到对应结束标签TagName的开始标签
lowerCasedTagName = tagName.toLowerCase()
for (pos = stack.length - 1; pos >= 0; post --) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) break
}
}
else {
pos = 0
}

if (pos >= 0) {
for (let i = stack.length - 1; i >= pos; i --) {
...
// 匹配的开始标签交给end回调函数处理
if (options.end) {
options.end(stack[i].tag, start, end)
}
}

// 将处理完的开始标签从stack中删掉
stack.length = pos
lastTag = pos && stack[pos - 1].tag
} else {
...
}
}
  1. 转换成AST

跑完解析HTML逻辑之后,就要开始正式生成AST树。在parse中,通过向parseHTML传入回调函数start、end、chars来将stack转换成AST树,最后再将AST赋值给root,返回给外部使用。
因此,我们只需要把精力集中在研究start、end、chars这三个函数上面即可。

1)start函数

在start函数中,主要做的事情就是创建一个ASTElement对象,解析动态属性,最后将ASTElement挂载到root下面。
大致代码如下:

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
// src/compiler/parser/index.ts
let inVPre = false
start(tag, attrs, unary, start, end) {
...
// 创建一个ASTElement
let element: ASTElement = createASTElement(tag, attrs, currentParent)
...

// 检查属性中有没有v-pre属性
if (!inVPre) {
processPre(element)
if (element.pre) {
inVPre = true
}
}

...

// 如果有v-pre属性,就将文本内容当作普通的文本内容,直接渲染
if (inVPre) {
processRawAttrs(element)
}
else if (!element.processed){
// 处理v-for、v-if、v-once属性
processFor(element)
processIf(element)
processOnce(element)
}

// 如果root为空,说明这个element是根节点,所以直接将element赋值给root即可
if (!root) {
root = element
...
}

// 非自闭合的标签可能有子节点需要处理,所以暂时入栈
if (!unary) {
// 将element保存为父元素
currentParent = element
stack.push(element)
}
// 如果是自闭合标签,就关闭元素节点
else {
closeElement(element)
}
}

其中createASTElement的具体代码如下:

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
export function createASTElement(
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
) {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
}

// 通过attrs数组生成一个map
function makeAttrsMap(attrs: Array<Record<string, any>>): Record<string, any> {
const map = {}
for (let i = 0, l = attrs.length; i < l; i ++) {
...
map[attrs[i].name] = attrs[i].value
}
return map
}

这个ASTElement就是AST树的单一元素。
其中type值为1代表这个元素节点,后面还会有type为2的模板变量节点,以及type为3的的文本节点。
tag就是标签名。
attrsList是以数组保存节点属性。
attrsMap是以键值对来保存节点属性。
parent就是父节点
childiren就是子节点

创建完ASTElement之后,就会判断是否是动态属性,这里会处理到的动态属性包括v-pre、v-for、v-if和v-once。
其中v-pre属性会让原本会被解析成模板变量的文本内容以普通文本的形式进行解析,而processPre函数内部就是给element添加一个pre=true的布尔变量。
具体代码如下:

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
// src/compiler/parser/index.ts
function processPre(el) {
if (getAndRemoveAttr(el, 'v-pre') != null) {
el.pre = true
}
}
export function getAndRemoveAttr(
el: ASTElement,
name: string,
removeFromMap?: boolean
): string | undefined {
let val
if ((val = el.attrsMap[name]) != null) {
const list = el.attrsList
// 删除list中的name属性
for (let i = 0, l = list.length; i < l; i ++) {
if (list[i].name === name) {
list.splice(i, 1)
break
}
}
}
// 删除Map的中name属性
if (removeFromMap) {
delete el.attrsMap[name]
}
return val
}

如果是inVPre为true,接下来的element就会交给函数processRawAttrs处理,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/compiler/parser/index.ts
function processRawAttrs(el) {
const list = el.attrsList
const len = list.length
if (len) {
const attrs: Array<ASTAttrs> = (el.attrs = new Array(len))
for (let i = 0; i < len; i ++) {
attrs[i] = {
name: list[i].name,
value: JSON.stringify(list[i].value)
}
if (list[i].start != null) {
attrs[i].start = list[i].start
attrs[i].end = list[i].end
}
}
else if (!el.pre) {
el.plain = true
}
}
}

这段代码就是给element增加一个attrs属性,用来保存文本内容。

接着是处理动态属性中的v-for、v-if和v-once:
a.先来看看processFor的代码:

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
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g

export function processFor(el: ASTElement) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
const res = parseFor(exp)
if (res) {
extend(el, res)
}
...
}
}
export function parseFor(exp: string): ForParseResult | undefined {
// 匹配for表达式in/of前后的变量
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
const res: any = {}
// 保存即将被for循环的变量
res.for = inMatch[2].trim()
// 因为ES6支持解构赋值,这里将解构赋值的左右括号去掉
const alias = inMatch[1].trim().replace(stripParentRE, '')
// 匹配多个变量,如果匹配到了就说明是解构赋值
const iteratorMatch = alias.match(forIteratorRE)
if (iteratorMatch) {
// 这里最多只匹配三个解构赋值变量,第一个赋值给alias,第二个给iterator1,第三个给iterator2
res.alias = alias.replace(forIteratorRE, '').trim()
res.iterator1 = iteratorMatch[1].trim()
if (iteratorMatch[2]) {
res.iterator2 = iteratorMatch[2].trim()
}
}
else {
res.alias = alias
}
return res
}
export function extend (
to: Record<PropertyKey, any>,
_from: Record<PropertyKey, any>
): Record<PropertyKey, any> {
for (let key in _from) {
to[key] = _from[key]
}
return to
}

processFor函数的核心就是调用parseFor函数来解析for循环表达式
在parseFor函数中用到了三个正则表达式,第一个forAliasRE用于匹配整个for循环表达式,并获取表达式前后的两个变量。
其可视化如下:

通过上图可以知道for表达式有in和of两种,前者取的是属性名,后者取的是属性值

这里将要进行循环的目标变量会被保存到res.for之中,而取出的变量会被保存到alias之中。
不过由于ES6支持使用解构赋值,所以这里专门处理了这种情况。
首先代码通过正则表达式:

1
const stripParensRE = /^\(|\)$/g

将解构赋值表达式的左右括号给去掉

接着再通过正则表达式:

1
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/

来匹配解构赋值中的第二个跟第三个变量,对,这里最多只匹配三个解构赋值变量,再多的变量就要考虑数组是不是存了太多东西了。
匹配到之后,会将它们分别保存在iterator1和iterator2之中。
最后将解析完成res返回给外部
processFor拿到parseFor返回的结果之后,就会通过extend函数将自己的属性添加到element上面来。

b.processIf函数

processIf用来处理v-if属性,具体代码如下:

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
function processIf(el) {
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
el.if = exp
// 往element中添加if对象
addIfCondition(el, {
exp,
block: el
})
}
else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}

export function addIfCondition(el: ASTElement, condition: ASTIfCondition) {
// 新增一个ifConditions属性
if (!el.ifConditions) {
el.ifConditions = []
}
el.ifConditions.push(condition)
}

条件动态属性有三种类型:v-if、v-else、v-elseif,其中v-if和v-elseif需要保存表达式,v-else只需要保存一个布尔值即可。

c.processOnce函数

具体代码如下:

1
2
3
4
5
6
function processOnce(el) {
const once = getAndRemoveAttr(el, 'v-once')
if (once != null) {
el.once = true
}
}

这段就很简单了,就是检测v-once,然后设置once属性

最后如果是自闭合标签,就要调用closeElement函数来结束该element的处理,大致代码如下:

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
function closeElement(element) {
// 去掉文本内容的空行元素
trimEndingWhiteSpace(element)
if(!inVPre && !element.processed) {
// 进一步处理element的动态属性
element = processElement(element, options)
}

if (!stack.length && element !== root) {
if (root.if && (element.elseif || element.else)) {
...
addIfCondition(root, {
exp: element.elseif,
block: element
})
}
...
}

if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else {
if (element.slotScope) {
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlot || (currentParent.scopedSlots = {})) [
name
] = element
}
currentParent.children.push(element)
element.parent = currentParent
}
}

element.children = element.children.filter(c => !c.slotScope)
trimEndingWhitespace(element)

if (element.pre) {
inVPre = false
}

if (platformIsPreTag(element.tag)) {
inPre = false
}

...
}

这里面的核心是processElement函数,它会进一步处理element中的动态属性,大致代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function processElement(element: ASTElement, options: CompilerOptions) {
processKey(element)

element.plain = !element.key && !element.scopedSlots && !element.attrsList.length

processRef(element)
processSlotContent(element)
processSlotOutlet(element)
processComponent(element)

...

processAttrs(element)

return element
}

其中processKey用来处理属性名为key的绑定属性,这个属性会在diff算法中用到。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function processKey(el) {
const exp = getBindingAttr(el, 'key')
if (exp) {
...
el.key = exp
}
}
function getBindingAttr(
el: ASTElement,
name: string,
getStatic?: boolean
): string | undefined {
const dynamicValue = getAndRemoveAttr(el, ':' + name) || getAndRemoveAttr(el, 'v-bind:' + name)
if (dynamicValue !== null) {
// 这里的parseFilters用于解析表达式形式的dynamicValue,可以暂时不用细究
return parseFilters(dynamicValue)
}
...
}

其中getBindingAttr函数就是获取以v-bind开头的动态属性。

processRef用来处理ref属性,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function processRef(el) {
const ref = getBindingAttr(el, 'ref')
if (ref) {
el.ref = ref
el.refInFor = checkInFor(el)
}
}

function checkInFor(el: ASTElement): boolean {
let parent: ASTElement | void = el
while (parent) {
if (parent.for !== undefined) {
return true
}

parent = parent.parent
}
return false
}

因为for循环会创建出来多个相同的节点,当然相同的ref也会有多个,所以需要增加一个refInFor来标识这个状态。

processSlotContent和processSlotOutlet用来处理模板变量template。
processSlotContent函数处理外部组件的template,其代码如下:

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
export const emptySlotScopeToken = `_empty_`
const slotRE = /^v-slot(:|$)|^#/
const dynamicArgRE = /^\[.*\]$/

function processSlotContent(el) {
let slotScope
if (el.tag === 'template') {
slotScope = getAndRemoveAttr(el, 'scope')
...
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
} else if (slotScope == getAndRemoveAttr(el, 'slot-scope')) {
...
el.slotScope = slotScope
}

const slotTarget = getBindingAttr(el, 'slot')
if (slotTarget) {
el.slotTarget = slotTarget === '""' ? 'default' : slotTarget
el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot'])

if (el.tag !== 'template' && !el.slotScope) {
addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
}
}

// 这里开始处理v-slot属性
if (process.env.NEW_SLOT_SYNTAX) {
if (el.tag === 'template') {
// 匹配v-slot或#开头的属性
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
...
// 获取插槽的名称
const { name, dynamic } = getSlotName(slotBinding)
el.slotTarget = name
el.slotTargetDynamic = dynamic
el.slotScope = slotBinding.value || emptySlotScopeToken
}
}
}
else {
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
...
// 插槽的模板节点保存在element的scopedSlots属性中
const slots = el.scopedSlots || (el.scopedSlots = {})
const { name, dynamic } = getSlotName(slotBinding)
const slotContainer = (slots[name] = createASTElement(
'template',
[],
el
))
slotContainer.slotTarget = name
slotContainer.slotTargetDynamic = dynamic
// 过滤掉那些没有设置插槽名称的节点
slotContainer.children = el.children.filter((c: any) => {
if (!c.slotTarget) {
c.parent = slotContainer
return true
}
})
slotContinaer.slotScope = slotBinding.value || emptySlotScopeToken
el.children = []
el.plain = false
}
}
}

function getSlotName(binding) {
let name = binding.name.replace(slotRE, '')
if (!name) {
// 如果name为空,那就是默认的default
if (binding.name[0] !== '#') {
name = 'default'
}
...
}
// 在2.6.0中加入了动态插槽,动态插槽即在左右中括号中间使用变量
return dynamicArgRE.test(name)
? { name: name.slice(1, -1), dynamic: true }
: { name: `"${name}"`, dynamic: true }
}

export function getAndRemoveAttrByRegex(el: ASTElement, name: RegExp) {
const list = el.attrsList
for(let i = 0, l = list.length; i < l; i ++) {
const attr = list[i]
if (name.test(attr.name)) {
list.splice(i, 1)
return attr
}
}
}

刚看到这段代码有点迷糊,这里面会专门处理slot-scope这个从来没见过的属性。
查阅文档之后发现slot-scope和slot在2.6.0以上版本已经被废弃了,而这里为了兼容性把相关逻辑保留下来了。
因此这部分可以不用细究,把主要精力放在v-slot解析逻辑上面。

processSlotOutlet函数处理的是内部组件的slot占位符,其代码如下:

1
2
3
4
5
6
function processSlotOutlet(el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name')
...
}
}

这个函数就很简单了,仅仅是判断一下标签名,然后新增一个slotName用来保存插槽的名称

processComponent用于处理is属性,具体代码如下:

1
2
3
4
5
6
7
function processComponent(el) {
let binding
if ((binding = getBindingAttr(el, 'is'))) {
el.component = binding
}
...
}

is属性允许在data中定义对应的变量,修改该变量可以快速切换Component

最后的processAttrs用于处理v-model、v-bind等等剩余,具体代码如下:

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
function processAttrs(el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for(i = 0, l = list.length; i < l; i ++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
el.hasBinding = true
// 匹配动态属性后跟着的修饰符,如.prevent、.stop等
modifiers = parseModifiers(name.replace(dirRE, ''))
// 同样是匹配修饰符
if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {
// 如果该属性是一个独立的修饰符,就新增一个prop属性设为true
(modifiers || (modifiers = {})).prop = true
name = `.` + name.slice(1).replace(modifierRE, '')
}
else if (modifiers) {
name = name.replace(modifierRE, '')
}

// 匹配v-bind
if (bindRE.test(name)) {
name = name.replace(bindRE, '')
value = parseFilters(value)
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)
}
...
if (modifiers) {
if (modifiers.prop && !isDynamic) {
name = camelize(name)
if (name == 'innerHTML') name = 'innerHTML'
}
if (modifiers.camel && !isDynamic) {
name = camelize(name)
}
// 处理sync修饰符下的v-bind属性,支持子组件向父组件传值的功能
if (modifiers.sync) {
syncGen = genAssignmentCode(value, '$event')
if (!isDynamic) {
addHandler(
el,
`update:${camelize(name)}`,
syncGen,
null,
false,
warn,
list[i]
)
if (hyphenate(name) !== camelize(name)) {
addHandler(
el,
`update:${hyphenate(name)}`,
syncGen,
null,
false,
warn,
list[i]
)
}
}
else {
addHandler(
el,
`"update:"+(${name})`,
syncGen,
null,
false,
warn,
list[i],
true
)
}
}
}
if (
(modifiers && modifiers.prop) ||
(!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name))
) {
addProp(el, name, value, list[i], isDynamic)
}
else {
addAttr(el, name, value, list[i], isDynamic)
}
}
// 处理v-on属性
else if (onRE.test(name)) {
name = name.replace(onRE, '')
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)
}
// 添加事件
addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
}
else {
name = name.replace(dirRE, '')
const argMatch = name.match(argRE)
let arg = argMatch && argMatch[1]
isDynamic = false
if (arg) {
name = name.slice(0, -(arg.length + 1))
if (dynamicArgRE.test(arg)) {
arg = arg.slice(1, -1)
isDynamic = true
}
}
addDirective(
el,
name,
rawName,
value,
arg,
isDynamic,
modifiers,
list[i]
)
...
}
}
else {
...
addAttr(el, name, JSON.stringify(value), list[i])
if (
!el.component &&
name === 'muted' &&
platformMustUseProp(el.tag, el.attrsMap.type, name)
) {
addProp(el, name, 'true', list[i])
}
}
}
}

// 将横杠转换成驼峰命名
const camelizeRE = /-(\w)/g
export const camlize = cached((str: string): string => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
})

export function cached<R>(fn: (str: string) => R): (str: string) => R {
const cache: Record<string, R> = Object.create(null)
return function cachedFn(str: string) {
const hit = cache[str]
return hit || (cache[str] = fn[str])
}
}

// 给element添加事件
export function addHandler(
el: ASTElement,
name: string,
value: string,
modifiers?: ASTModifiers | null,
important?: boolean,
warn?: Function,
range?: Range,
dynamic?: boolean
) {
modifiers = modifiers || emptyObject

....

// 右键事件
if (modifiers.right) {
if (dynamic) {
name = `(${name})==='click'?'contextmenu':(${name})`
}
else if (name === 'click') {
name = 'contextmenu'
delete modifiers.right
}
}
else if (modifiers.middle) {
if (dynamic) {
name = `(${name}) === 'click' ? 'mouseup' : (${name})`
}
else if (name === 'click') {
name = 'mouseup'
}
}

// 捕获事件
if (modifiers.capture) {
delete modifiers.capture
name = prependModifierMarker('!', name, dynamic)
}
// 只触发一次
if (modifiers.once) {
delete modifiers.once
name = prependModifierMarker('~', name, dynamic)
}
// 不阻止默认事件
if (modifiers.passive) {
delete modifiers.passive
name = prependModifierMarker('&', name, dynamic)
}

let event
// 原生事件
if (modifiers.native) {
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
}
// 普通事件
else {
events = el.events || (el.events = {})
}

const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers
}

const handlers = events[name]
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
}
else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
}
else {
events[name] = newHandlers
}

el.plain = false
}

// 添加修饰符
export function addProp(
el: ASTElement,
name: string,
value: string,
range?: Range,
dynamic?: boolean
) {
(el.props || (el.props = [])).push(
rangeSetItem({ name, value, dynamic }, range)
)
el.plain = false
}

// 添加属性
export function addAttr(
el: ASTElement,
name: string,
value: any,
range?: Range,
dynamic?: boolean
) {
const attrs = dynamic
? el.dynamicAttrs || (el.dynamicAttrs = [])
: el.attrs || (el.attrs = [])
attrs.push(rangeSetItem({ name, value, dynamic }, range))
el.plain = false
}

export functio addDirective(
el: ASTElement,
name: string,
rawNama: string,
value: string,
arg?: string,
isDynamicArg?: boolean,
modifiers?: ASTModifiers,
range?: Range
) {
(el.directives || (el.directives = [])).push(
rangeSetItem(
{
name,
rawName,
value,
arg,
isDynamicArg,
modifiers
},
range
)
)
el.plain = false
}

这里的cached利用闭包函数的特性,将计算出来的结果保存在内存中,下次取用的时候直接返回结果,无需重复计算,估计computed也是这么实现的。
其他部分就是匹配v-、@、:、#这些Vue独有的动态属性,并向element的props、attrs和directives属性填充对应的值。

2)chars函数

chars函数主要做的事情就是解析节点中的文本内容,文本内容分为两种,
一种是普通的文本内容。
另一种是Vue特定的模板变量,也就是用双尖括号囊括文本内容。
大致代码如下:

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
const lineBreakRE = /[\r\n]/
const whitespaceRE = /[ \f\t\r\n]+/g

chars(text: string, start?: number, end?: number) {
...
// IE浏览器、标签名为textarea且带有placeholder属性的情况下不处理
if (
isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}

const children = currentParent.children
if (inPre || text.trim()) {
// 如果是script或style标签,那么其文本内容就是JS代码或者CSS代码,不可以decode其文本内容
text = isTextTag(currentParent)
? text
: (decodeHTMLCached(text) as string)
}
else if (!children.length) {
text = ''
}
else if (whiteSpaceOption) {
if (whiteSpaceOption === 'condense') {
text = lineBreakRE.test(text)? '' : ' '
}
else {
text = ' '
}
}
else {
text = preserveWhitespace ? ' ' : ''
}

if (text) {
if (!inPre && whitespaceOption === 'condense') {
text = text.replace(whitespaceRE, ' ')
}
let res
let child: ASTNode | undefined
// parseText用于检测文本内容是否为模板变量
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
// 带有模板变量的类型定为2
type: 2,
// 单独保存模板变量的表达式
expression: res.expression,
tokens: res.tokens,
text
}
} else if (
text !== ' '||
!children.length ||
children[children.length - 1].text !== ' '
) {
child = {
// 普通文本类型定位3
type: 3,
text
}
}
if (child) {
...
children.push(child)
}
}
}

function isTextTag(el): boolean {
return el.tag === 'script' || el.tag === 'style'
}

// 匹配双尖括号
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
export function parseText(
text: string,
delimiters?: [string, string]
): TextParserResult | void {
// 这里主要用到defaultRE来匹配模板变量,前者一般用不到
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
if (!tagRE.test(text)) {
return
}
const tokens: string[] = []
const rawTokens: any[] = []
let lastIndex = (tagRE.lastIndex = 0)
let match, index, tokenValue
while((match = tagRE.exec(text))) {
// index匹配到字符的起始位置
index = match.index
// 取到两个表达式之间的文本内容
if (index > lastIndex) {
rawTokens.push((tokenValue = text.slice(lastIndex, index)))
tokens.push(JSON.stringify(tokenValue))
}
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })
// lastIndex代表匹配的文本内容的最后一位
lastIndex = index + match[0].length
}

if (lastIndex < text.length) {
rawTokens.push((tokenValue = text.slice(lastIndex)))
tokens.push(JSON.stringify(tokenValue))
}

return {
expression: tokens.join('+'),
tokens: rawTokens
}
}

const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g
const buildRegex = cached(delimiters => {
const open = delimiters[0].replace(regexEscapeRE, '\\$&')
const close = delimiters[1].replace(regexEscapeRE, '\\$&')
return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
})

为了区分模板变量跟普通文本,代码使用了parseText函数。
而在parseText函数中,一如既往使用正则表达式defaultTagRE来匹配双尖括号:
匹配到之后,就创建tokens和rawTokens两个属性来储存模板变量。
tokens保存的是拼接字符串。
rawTokens保存的是以@binding为键的对象。
最后用lastIndex记录文本内容的最后一位,再通过循环来匹配下一个模板变量。
当匹配到新的模板变量,match.index将会保存第一个字符的序号。
这时只要判断下index > lastIndex,就可以获取到两个模板变量之间的文本内容,直接保存到tokens和rawTokens中。

循环结束之后,再处理下模板变量后的文本内容,返回结果给res
当res不为空的时候,节点child的type就为2,并保存tokens和rawTokens变量。为空的时候,节点child的type就为3。
拿到child之后,就将child挂到currentParent中。

3)end函数

end函数用于做节点的最终处理,大致代码如下:

1
2
3
4
5
6
7
end(tag, start, end) {
const element = stack[stack.length - 1]
stack.length -= 1
currentParent = stack[stack.length - 1]
...
closeElement(element)
}

获取最后一个元素,将最后一个元素交给closeElement处理
然后将最后一个元素从stack里面出栈,这时最后一个元素就是其父元素,保存到currentParent之中。

五、优化器Optimizer

Optimizer的作用就是标记静态节点,被标记的节点不会参与渲染,这样将会很好的节省性能。
其大致代码如下:

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
export function optimize (
root: ASTElement | null | undefined,
options: CompilerOptions
) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
...
// 标记所有静态子节点
markStatic(root)
// 标记根节点为静态节点
markStaticRoots(root, false)
}

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

const genStaticKeysCached = cached(genStaticKeys)

function markStatic(node: ASTNode) {
// 判断是否是静态节点
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
}
}
if (node.ifConditions) {
for (let i = 0, l = node.ifCondition.length; i < l; i ++) {
const block = node.ifCondition[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}

function markStaticRoots(node: ASTNode, isInFor: boolean) {
// 只判断元素节点
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 (let i = 0, l = node.children.length; i < l; i ++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}

// ifConditions暂时看不懂为啥要再遍历一次
if (node.ifConditions) {
for (let i = 0, l = node.ifConditions.length; i < l; i ++) {
markStaticRoots(node.ifConditions[i], isInFor)
}
}
}
}

function isStatic(node: ASTNode): boolean {
// 模板变量都是动态节点
if (node.type === 2) {
return false
}
// 普通文本都是静态节点
if (node.type === 3) {
return true
}

// 其他条件
return !!(
// pre强制按普通文本解析
node.pre ||
(!node.hasBindings && // 没有动态属性
!node.if && // 没有if
!node.for && // 没有for
!isBuiltInTag(node.tag) &&
isPlatformReservedTag(node.tag) &&
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
)
)
}

export const isBuildInTag = makeMap('slot,component', true)
export const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')

function isDirectChildOfTemplateFor(node: ASTElement): boolean {
while(node.parent) {
node = node.parent
if (node.tag !== 'template') {
return false
}
if (node.for) {
return true
}
}

return false
}

从上面代码可以看到,标记节点主要标记两类节点,第一类是普通静态节点,第二类是静态根节点

从根节点开始,取它的子节点进行遍历,接着递归子节点的子节点,直到不再有子节点。
普通静态节点的判断条件是:
1)节点类型是3,也即是普通的文本内容
2)没有任何动态属性
3)没有任何Vue特有标签,如slot、template
4)外部强制打静态标记,即isStaticKey

静态根节点的标记也是差不多的逻辑,其判断条件是:
1)节点类型是1,即元素节点
2)子节点只有一个并且是普通的文本节点
3)子节点是静态节点

六、代码生成器Code generator

Code generator主要用来生成代码字符串,这段代码字符串将在后续丢给JS引擎执行,生成虚拟DOM。
这样做有几个好处:
1)用字符串保存更节约空间
2)交给JS引擎执行不用考虑作用域的问题
3)性能耗费低

具体逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export type CodegenResult = {
render: string,
staticRenderFns: Array<string>
}

export function generate(
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)

const code = ast
? ast.tag === 'script'
? 'null'
: genElement(ast, state)
: '_c("div")'

return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}

generate函数的核心就是genElement函数,用来生成最终的代码字符串。
接着返回一个对象,对象中包含一个函数render,这个render函数就是用来渲染并生成虚拟DOM的。
genElement的具体代码如下:

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
export function genElement(el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}

// 生成静态根节点字符串
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
}
// 生成Once节点字符串
else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
}
// 生成For节点字符串
else if (el.for && !el.forProcessed) {
return genFor(el, state)
}
// 生成if节点字符串
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'
}
// 生成slot节点字符串
else if (el.tag === 'slot') {
return genSlot(el, state)
}
else {
let code
// 生成组件字符串
if (el.component) {
code = genComponent(el.component, el, state)
}
else {
let data
const maybeComponent = state.maybeComponent(el)
if (!el.plain || (el.pre && maybeComponent)) {
data = genData(el, state)
}

let tag: string | undefined
const bindings = state.options.bindings
if (maybeComponent && bindings && bindings.__isScriptSetup !== false) {
tag = checkBindingType(bindings, el.tag)
}
if (!tag) tag = `'${el.tag}'`

const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c(${tag}${
data ? `,${data}` : ''
}${
children ? `,${children}` : ''
})`
}
...
return code
}
}

其中最主要的函数包括genChildren、genData、genText,
像如下这样一段HTML代码:

1
<p title="Sherwood" @click="c">1</p>

生成的节点格式大致如此:

1
2
3
4
5
6
7
8
9
10
with(this) {
return _c (
'p',
{
attrs: {'title': 'Sherwood'},
on: {'click': c}
},
[_v('1')]
)
}

以下是各种生成字符串函数的代码:

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
export function genData(el: ASTElement, state: CodegenState): string {
let data = '{'

const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','

if (el.key) {
data += `key:${el.key},`
}

if (el.ref) {
data += `key:${el.ref},`
}

if (el.refInFor) {
data += `refInFor:true,`
}

if (el.pre) {
data += `pre:true,`
}

if (el.component) {
data += `tag:"${el.tag}",`
}

for (let i = 0; i < state.dataGenFns.length; i ++) {
data += state.dataGenFns[i](el)
}

if (el.attrs) {
data += `attrs:${genProps(el.attrs)},`
}

if (el.props) {
data += `domProps:${genProps(el.props)},`
}

if (el.events) {
data += `${genHandlers(el.events, false)},`
}

if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)},`
}

if (el.slotTarget && !el.slotScope) {
data += `slot:${el.slotTarget},`
}

if (el.scopedSlots) {
data += `${genScopedSlots(el, el.scopedSlots, state)},`
}

if (el.model) {
data += `model:{value:${el.model.value},callback:${el.model.callback},expression:${el.model.expression}},`
}

if (el.inlineTemplate) {
const inlineTemplate = genInlineTemplate(el, state)
if (inlineTemplate) {
data += `${inlineTemplate},`
}
}

data = data.replace(/,$/, '') + '}'

if (el.dynamicAttrs) {
data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
}

if (el.wrapData) {
data = el.wrapData(data)
}

if (el.wrapListeners) {
data = el.wrapListeners(data)
}

return data
}

export function genChildren(
el: ASTElement,
state: CodegenState,
checkSkip?: boolean,
altGenElement?: Function,
altGenNode?: Function
): string | void {
const children = el.children
...
return `[${children.map(c => genNode(c, [state])).join(',')}]`...
}

export function genNode(node: ASTNode, state: CodegenState): string {
if (node.type === 1) {
return genElement(node, state)
}
else if (node.type === 3 && node.isComment) {
return genComment(node)
}
else {
return genText(node)
}
}

export function genText(text: ASTText | ASTExpression): string {
return `_v(${
text.type === 2
? text.expression
: transformSpecialNewlines(JSON.stringify(text.text))
})`
}

在genData中,会生成各种属性相关的对象字符串。
接着调用genChildren生成子节点,genChildren会通过genNode来区分不同类型的节点。
如果是元素节点,就调用genElement进行递归处理
如果是文本节点,就调用genText生成。
这些下划线开头的函数,其实是各种方法的缩写,它们主要定义在src/core/instance/render-helpers/index.ts,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/core/instance/render-helpers/index.ts
export function installRenderHelpers(target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndex
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}

其中常用的有用于创建文本节点的_v,用于创建空节点的_e。
当然也有例外,比如_c,用于创建节点,定义在src/core/instance/render.ts,具体代码如下:

1
2
// src/core/instance/render.ts
vm._c(a, b, c, d) => createElement(vm, a, b, c, d, false)

七、总结

通过以上对源码的解析,已经完全了解模板编译的整个过程。

现在大致总结一下模板编译的核心要点:

  1. 通过parse函数生成AST树,通过optimize函数标记静态节点,通过generate函数生成代码字符串
  2. 在parse函数中调用了parseHTML来解析HTML
  3. parseHTML将会借助正则表达式来分别匹配开始标签、文本内容以及结束标签。
  4. 这里的开始标签匹配到之后,会push到stack中,之后在处理结束标签的时候,找到开始标签并出栈
  5. 在处理开始标签、文本标签和结束标签时,最终会将结果返回给回调函数start、chars、end
  6. 在回调函数中会将节点转换成ASTElement,最后生成AST树,返回给外部
  7. 生成AST树之后,就交给optimize进行优化。
  8. optimize会标记所有AST树静态节点,接着标记所有静态根节点
  9. 标记完成之后,会将AST树交给generate生成代码字符串
  10. 这些代码字符串会被保存在render函数中
  11. 当监听器Watcher监听到数据变化,就会调用render函数生成虚拟节点,然后通过对比新旧虚拟节点之间的区别,将最新的变化应用到真实DOM上面去

参考

Vue的模板编译原理
Vue 模板编译原理
Vue源码
如何看待Vue.js 2.0 的模板编译使用了with(this)的语法?
Vue中的cached函数
with文档