vue源码阅读(五) $mount 【compile parse函数生成AST】

现在我们来看 vm.$options.render 是在哪里定义的?

在 entry-runtime-with-compiler.js中我们可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const options = this.$options
....
if (!options.render) {
.....
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
.....
}

👇我们来看看compileToFunctions

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

然后我们就来到了本文的重点compile 函数

compile 函数(src/compiler/index.js)就是将 template 编译成 render function 的字符串形式

1
2
3
4
5
6
7
8
9
10
11
12
13
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options) //1. parse
optimize(ast, options) //2.optimize
const code = generate(ast, options) //3.generate
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})

createCompiler 函数主要通过3个步骤:parse、optimize、generate来生成一个包含ast、render、staticRenderFns的对象传给createCompilerCreator方法。

下面我们先看看parse:

1
2
> const ast = parse(template.trim(), options)
>

parse — 把HTML转换成AST树

AST 的全称是 Abstract Syntax Tree(抽象语法树),是源代码的抽象语法结构的树状表现形式,计算机学科中编译原理的概念。Vue 源码中借鉴 jQuery 作者 John Resig 的 HTML Parser 对模板进行解析,得到的就是 AST 代码。

Vue的AST节点

Vue自己是怎么来定义每个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
declare type ASTNode = ASTElement | ASTText | ASTExpression;

// 元素节点
declare type ASTElement = {
type: 1;
tag: string;
attrsList: Array<{ name: string; value: string }>;
attrsMap: { [key: string]: string | null };
parent: ASTElement | void;
children: Array<ASTNode>;
...
}

// 表达式节点
declare type ASTExpression = {
type: 2;
expression: string;
text: string;
static?: boolean;
// 2.4 ssr optimization
// 2.4+ 增加了对ssr的标识
ssrOptimizability?: number;
};

// 文本节点
declare type ASTText = {
type: 3;
text: string;
static?: boolean;
isComment?: boolean;
// 2.4 ssr optimization
// 2.4+ 增加了对ssr的标识
ssrOptimizability?: number;
};

Vue的AST节点有3类: ASTElement | ASTText | ASTExpression, 通过type来标记

parse函数

采用了 jQuery 作者 John ResigHTML Parser ,将 template字符串解析成 AST。。

基本流程图

WechatIMG5

下面开始看parse函数的主要部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function parse(template) {
...
const stack = [];
let currentParent; //当前父节点
let root; //最终返回出去的AST树根节点
...
parseHTML(template, {
// start方法就是处理 element 元素的过程。确定命名空间;创建AST元素 element;
// 执行预处理;定义root;处理各类 v- 标签的逻辑;最后更新 root、currentParent、stack 的结果。
// 最终通过 createASTElement 方法定义了一个新的 AST 对象
start: function start(tag, attrs, unary) {
......
},
end: function end() {
......
},
chars: function chars(text) {
......
}
})
return root
}

parseHTML

我们可以发现关键是parseHTML,下面看下parseHTML

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
export function parseHTML (html, options) {
...
while (html) {
// 保留 html 副本
last = html
/* 首先通过while (html)去循环判断html内容是否存在。
* 再判断文本内容是否在script/style标签中
* 上述条件都满足的话,开始解析html字符串*/
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
if (textEnd === 0) {
...
// End tag:
//判断此段html是否结束标签
// const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
// const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}

// Start tag:
// 匹配开始标签,获取match对象
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
}
}
}
}

我们从标签开始的匹配开始看,这里我们先关注下parseStartTag和handleStartTag函数

假设我们传递这样一个html字符串<div id="demo"></div>

parseStartTag函数

处理匹配到的开始标签部分(

),生成一个记录所有状态的对象match

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 parseStartTag () {
//判断html中是否存在开始标签
// const startTagOpen = new RegExp(`^<${qnameCapture}`)
const start = html.match(startTagOpen)
if (start) {
// 定义 match 结构
const match = {
tagName: start[1],
attrs: [],
start: index
}
advance(start[0].length)
let end, attr
// const startTagClose = /^\s*(\/?)>/
// 如果还没有到结束标签的位置
// 存入属性
// const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
while (!(end = html.match(startTagClose))
&& (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {

attr.start = index
advance(attr[0].length)
attr.end = index
match.attrs.push(attr)
}
// 返回处理后的标签match结构
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}

返回的match应为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
> {
> tagName:'div',
> attrs:[[
> '',
> 'id',
> '=',
> 'demo',
> undefined,
> undefined
> start:6
> end:14,
> groups: undefined,
> index:0,
> input: " id="demo">{{msg}}</div>"
> ]],
> start:0,
> end:28
> unarySlash:''
> }
>

handleStartTag函数

下面看下handleStartTag函数,handleStartTag对match接着进行了改造

接着处理上一步返回的match对象, 对match改造成易用的对象, 去掉不需要的属性

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
function handleStartTag (match) {
// match 是上面调用方法的时候传递过来的数据结构
const tagName = match.tagName
const unarySlash = match.unarySlash

if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}

// 是否是一元元素
const unary = isUnaryTag(tagName) || !!unarySlash

// 备份属性数组的长度
const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
// 取定义属性的值
const value = args[3] || args[4] || args[5] || ''
const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines

// 改变attr的格式为 [{name: 'id', value: 'demo'}]
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
}

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

// stack中记录当前解析的标签
// 如果不是自闭和标签
// 这里的stack这个变量在parseHTML中定义,作用是为了存放标签名 为了和结束标签进行匹配的作用。
if (options.start) {
// parse 函数传入的 start 方法
options.start(tagName, attrs, unary, match.start, match.end)
}
}

进过再次处理后mack函数为

1
2
3
4
5
6
7
8
> {
> tagName ='' div'
> attrs = [{name: 'id', value: 'demo'}]
> unary=false
> match.start = 0
> match.end = 28
> }
>

到这里似乎一切明朗了许多,parseHTML主要用来蚕食html字符串,解析出字符串中的tagNameattrsmatch等元素,传入start方法:

start方法

看下start(start 是从函数外部传入的函数),在compiler/parser/index.js中可见

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
// start方法就是处理 element 元素的过程。确定命名空间;创建AST元素 element;执行预处理;定义root;
// 处理各类 v- 标签的逻辑;最后更新 root、currentParent、stack 的结果。
// 最终通过 createASTElement 方法定义了一个新的 AST 对象
start (tag, attrs, unary) {
...
// 创建基础的 ASTElement
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
...

if (!inVPre) {
// 判断有没有 v-pre 指令的元素。如果有的话 element.pre = true
// 官网有介绍:<span v-pre>{{ this will not be compiled }}</span>
// 跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
// 处理原始属性
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
// v-for v-if v-once 结构指令
processFor(element)
processIf(element)
processOnce(element)
}

// 检查根节点约束
function checkRootConstraints (el) {
if (process.env.NODE_ENV !== 'production') {
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.'
)
}
if (el.attrsMap.hasOwnProperty('v-for')) {
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.'
)
}
}
}

// tree management
if (!root) {
// 如果不存在根节点
root = element
checkRootConstraints(root)
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
}

通过createASTElement创建一个新的ASTElement对象, 并进行一系列操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function createASTElement (
tag: string,
attrs: Array<Attr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
parent,
children: []
}
}

最终生成的AST语法树对象:

parse-end