vue源码分析(十一) 编译之解析(parse)——parse
# 1. 概述
分析完 parseHTML
我们再回到 scr/compiler/parse/index.js
,继续分析 parse
的源代码。
分析 parse
源码,我们还是以 第九章 (opens new window) 的案例代码为例进行,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>parse</title>
<script src="./vue.js"></script>
</head>
<body>
<div id="app">{{ fullName }}</div>
<script>
new Vue({
el: '#app',
template: `
<ul :class="classObject" class="list" v-show="isShow">
<li v-for="(l, i) in list" :key="i" @click="clickItem(index)">{{ i }}:{{ l }}</li>
</ul>
`,
data: {
isShow: true,
list: ['Vue', 'React', 'Angular'],
classObject: {
active: true,
'text-danger': false
}
},
methods: {
clickItem(index) {
console.log(index)
}
}
})
</script>
</body>
</html>
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
# 2. 整体结构
在分析源码之前我们,先以为代码的方式梳理一下 parse
的整理结构,如下:
export function createASTElement (
tag: string,
attrs: Array<Attr>,
parent: ASTElement | void
): ASTElement {
/* 省略... */
}
// HTML字符串转换为AST
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
/*
* 省略...
* 省略的代码用来初始化一些变量的值,以及创建一些新的变量,其中包括 root 变量,该变量为 parse 函数的返回值,即 AST
*/
// 警告日志函数
function warnOnce (msg, range) {
/* 省略... */
}
// 关闭节点
function closeElement (element) {
/* 省略... */
}
// 删除尾部空白节点
function trimEndingWhitespace (el) {
/* 省略... */
}
// 校验根节点
function checkRootConstraints (el) {
/* 省略... */
}
parseHTML(template, {
// 其他选项...
start (tag, attrs, unary, start, end) {
/* 省略... */
},
end (tag, start, end) {
/* 省略... */
},
chars (text: string, start: number, end: number) {
/* 省略... */
},
comment (text: string, start, end) {
/* 省略... */
}
})
return root
}
// 处理 v-pre
function processPre (el) {/* 省略...*/}
function processRawAttrs (el) {/* 省略...*/}
// 处理 element
export function processElement (element: ASTElement, options: CompilerOptions) {/* 省略...*/}
// 处理 v-key
function processKey (el) {/* 省略...*/}
// 处理 ref
function processRef (el) {/* 省略...*/}
// 处理 v-for
export function processFor (el: ASTElement) {/* 省略...*/}
// 解析 v-for
export function parseFor (exp: string): ?ForParseResult {/* 省略...*/}
// 处理 v-if
function processIf (el) {/* 省略...*/}
// 处理 if 条件
function processIfConditions (el, parent) {/* 省略...*/}
// 找到 v-pre 中的值
function findPrevElement (children: Array<any>): ASTElement | void {/* 省略...*/}
// v-if的条件数组添加
export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {/* 省略...*/}
// 处理 v-once
function processOnce (el) {/* 省略...*/}
// 处理 slot
function processSlotContent (el) {/* 省略...*/}
function getSlotName (binding) {/* 省略...*/}
function processSlotOutlet (el) {/* 省略...*/}
// 处理 is 特性
function processComponent (el) {/* 省略...*/}
// 处理 attrs 熟悉
function processAttrs (el) {/* 省略...*/}
// 检查是否在 v-for中
function checkInFor (el: ASTElement): boolean {/* 省略...*/}
function parseModifiers (name: string): Object | void {/* 省略...*/}
function makeAttrsMap (attrs: Array<Object>): Object {/* 省略...*/}
// 是否是 text 标签,即script,style标签,不会解析
function isTextTag (el): boolean {/* 省略...*/}
// 是否是禁用标签
function isForbiddenTag (el): boolean {/* 省略...*/}
// 修复ie svg的bug
function guardIESVGBug (attrs) {/* 省略...*/}
// 检查v-model在for循环中的绑定的检查
function checkForAliasModel (el, value) {/* 省略...*/}
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
分析完 src/compiler/parser/index.js
文件的整体结构。接下来我们回过头来,从文件的开始部分来分析。
# 3. 正则常量分析
下面我们逐一分析一下这一系列常量。
# 3.1 onRE
源码目录:src/compiler/parser/index.js
export const onRE = /^@|^v-on:/
这个常量用来匹配以字符 @
或 v-on:
开头的字符串,主要作用是检测标签属性名是否是监听事件的指令。
# 3.2 dirRE
源码目录:src/compiler/parser/index.js
export const dirRE = process.env.VBIND_PROP_SHORTHAND
? /^v-|^@|^:|^\.|^#/
: /^v-|^@|^:|^#/
2
3
它用来匹配以字符 v-
或 @
或 :
或 .
或 #
开头的字符串,主要作用是检测标签属性名是否是指令。所以通过这个正则我们可以知道,在 vue
中所有以 v-
开头的属性都被认为是指令,另外 @
字符是 v-on
的缩写,:
字符是 v-bind
的缩写。
# 3.3 forAliasRE
源码目录:src/compiler/parser/index.js
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
该正则包含三个分组,
- 第一个分组为
([\s\S]*?)
,该分组是一个惰性匹配的分组,\s
空白符\S
非空白符,即它匹配的内容为任何字符,包括换行符等。 - 第二个分组为
(?:in|of)
,该分组用来匹配字符串in
或者of
,并且该分组是非捕获的分组。 - 第三个分组为
([\s\S]*)
,与第一个分组类似,不同的是第三个分组是非惰性匹配。
同时每个分组之间都会匹配至少一个空白符 \s+
。通过以上说明可知,正则 forAliasRE
用来匹配 v-for
属性的值,并捕获 in
或 of
前后的字符串。假设我们像如下这样使用 v-for
:<div v-for="obj of list"></div>
,那么正则 forAliasRE
用来匹配字符串 'obj of list'
,并捕获到两个字符串 'obj'
和 'list'
。
# 3.4 forIteratorRE
源码目录:src/compiler/parser/index.js
'
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
该正则用来匹配 forAliasRE
包含两个分组:
- 第一个捕获组用来捕获一个不包含字符
,``}
和]
的字符串,且该字符串前面有一个字符,
,如:', index'
。 - 第二个分组为非捕获的分组,第三个分组为捕获的分组,其捕获的内容与第一个捕获组相同。
举几个例子,我们知道 v-for
有几种不同的写法,其中一种使用 v-for
的方式是:
<div v-for="obj of list"></div>
如果像如上这样使用 v-for
,那么 forAliasRE
正则的第一个捕获组的内容为字符串 'obj'
,此时使用 forIteratorRE
正则去匹配字符串 'obj'
将得不到任何内容。
第二种使用 v-for
的方式为:
<div v-for="(obj, index) of list"></div>
此时 forAliasRE
正则的第一个捕获组的内容为字符串 '(obj, index)'
,如果去掉左右括号则该字符串为 'obj, index'
,如果使用 forIteratorRE
正则去匹配字符串 'obj, index'
则会匹配成功,并且 forIteratorRE
正则的第一个捕获组将捕获到字符串 'index'
,但第二个捕获组捕获不到任何内容。
第三种使用 v-for
的方式为:
<div v-for="(value, key, index) in object"></div>
以上方式主要用于遍历对象而非数组,此时 forAliasRE
正则的第一个捕获组的内容为字符串 '(value, key, index)'
,如果去掉左右括号则该字符串为 'value, key, index'
,如果使用 forIteratorRE
正则去匹配字符串 'value, key, index'
则会匹配成功,并且 forIteratorRE
正则的第一个捕获组将捕获到字符串 'key'
,但第二个捕获组将捕获到字符串 'index'
。
# 3.5 stripParensRE
源码目录:src/compiler/parser/index.js
const stripParensRE = /^\(|\)$/g
用来匹配要么以字符 (
开头,要么以字符 )
结尾的字符串,或者两者都满足。例如在 v-for
中去除括号。
# 3.6 dynamicArgRE
源码目录:src/compiler/parser/index.js
const dynamicArgRE = /^\[.*\]$/
用来匹配以字符 [
开头并以字符 ]
结尾的字符串,作用是判断是否为动态属性。
.
:匹配除换行符 \n
之外的任何单字符。要匹配 .
,请使用 \.
。
*
:匹配前面的子表达式零次或多次。要匹配 *
,请使用 \*
。
# 3.7 argRE
源码目录:src/compiler/parser/index.js
const argRE = /:(.*)$/
正则 argRE
用来匹配指令中的参数,如下:
<div v-on:click.stop="handleClick"></div>
其中 v-on
为指令,click
为传递给 v-on
指令的参数,stop
为修饰符。所以 argRE
正则用来匹配指令编写中的参数,并且拥有一个捕获组,用来捕获参数的名字。
# 3.8 bindRE
源码目录:src/compiler/parser/index.js
export const bindRE = /^:|^\.|^v-bind:/
该正则用来匹配以字符 :
或字符串 v-bind:
或字符串 .
开头的字符串,主要用来检测一个标签的属性是否是绑定(v-bind
)。
# 3.9 propBindRE
源码目录:src/compiler/parser/index.js
const propBindRE = /^\./
该正则用来匹配以符串 .
开头的字符串,主要用来检测一个(v-bind
)指令是否绑定修饰符(.prop
)。
说明:关于 .prop
请参考 v-bind (opens new window) 。
# 3.10 modifierRE
源码目录:src/compiler/parser/index.js
const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g
用来匹配以字符 (v-bind
)指令是否绑定修饰符,并且捕获匹配到的字符串,如下例子:
<svg :view-box.camel="viewBox"></svg>
在代码中经过其他过滤,用来匹配此正则表达式的字符串为 :view-box.camel
,所以最终匹配到的是 .camel
。
# 3.11 slotRE
源码目录:src/compiler/parser/index.js
const slotRE = /^v-slot(:|$)|^#/
用来匹配以字符 (v-slot
) 或 字符 #
开头的字符串,并且捕获匹配到的字符串中的 :
字符。
# 3.12 lineBreakRE
源码目录:src/compiler/parser/index.js
const lineBreakRE = /[\r\n]/
匹配换行符和回车符。
# 3.13 whitespaceRE
源码目录:src/compiler/parser/index.js
const whitespaceRE = /\s+/g
匹配任何空白字符一次或多次,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]
。注意Unicode
正则表达式会匹配全角空格符。
# 3.14 invalidAttributeRE
源码目录:src/compiler/parser/index.js
const invalidAttributeRE = /[\s"'<>\/=]/
匹配 空白 或 "
或 '
或 <
或 >
或 /
或 =
字符。
# 3.15 解码函数
源码目录:src/compiler/parser/index.js
const decodeHTMLCached = cached(he.decode)
cached
作用是接收一个函数作为参数并返回一个新的函数,新函数的功能与作为参数传递的函数功能相同,唯一不同的是新函数具有缓存值的功能,如果一个函数在接收相同参数的情况下所返回的值总是相同的,那么 cached
函数将会为该函数提供性能提升的优势。
可以看到传递给 cached
函数的参数是 he.decode
函数,其中 he
为第三方的库,he.decode
函数用于 HTML
字符实体的解码工作,如:
console.log(he.decode('&')) // & -> '&'
由于字符实体 &
代表的字符为 &
。所以字符串 &
经过解码后将变为字符 &
。decodeHTMLCached
函数在后面将被用于对纯文本的解码,如果不进行解码,那么用户将无法使用字符实体编写字符。
# 3.16 平台化选项变量
源码目录:src/compiler/parser/index.js
// configurable state
// 日志输出函数
export let warn: any
// 改变纯文本插入分隔符。修改指令的书写风格,比如默认是{{mgs}} delimiters: ['${', '}']之后变成这样 ${mgs}
let delimiters
// transforms 样式属性的集合函数
let transforms
// transforms arr属性的集合 函数
let preTransforms
// 空数组
let postTransforms
// 判断标签是否是pre 如果是则返回真
let platformIsPreTag
// 来检测一个属性在标签中是否要使用元素对象原生的 prop 进行绑定
let platformMustUseProp
// 来获取元素(标签)的命名空间,即判断 tag 是否是svg或者math 标签
let platformGetTagNamespace
// 判断是组件
let maybeComponent
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 4. createASTElement 函数
createASTElement
函数的作用就是方便我们创建一个节点,或者说方便我们创建一个元素的描述对象,如下:
源码目录:src/compiler/parser/index.js
export function createASTElement (
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
此函数接收三个参数,我们以概述里面的案例来分别是讲解,如下:
tag
:标签名字tag
,如ul
。attrs
:标签拥有的属性数组, 如[{name: ':class', value: 'classObject'}, {name: 'class', value: 'list'}, {name: 'v-show', value: 'isShow'}]
。parent
:标签的父标签描述对象。
返回一个对象,其中 attrsMap
是通过调用 makeAttrsMap(attrs)
函数得到的,下面我们看一下 makeAttrsMap
的源码,如下:
源码目录:src/compiler/parser/index.js
function makeAttrsMap (attrs: Array<Object>): Object {
const map = {}
for (let i = 0, l = attrs.length; i < l; i++) {
if (
process.env.NODE_ENV !== 'production' &&
map[attrs[i].name] && !isIE && !isEdge
) {
warn('duplicate attribute: ' + attrs[i].name, attrs[i])
}
map[attrs[i].name] = attrs[i].value
}
return map
}
2
3
4
5
6
7
8
9
10
11
12
13
可以看出 makeAttrsMap
函数的作用就是通过循环属性数组将标签的属性数组转换成健值对。例如:[{name: ':class', value: 'classObject'}, {name: 'class', value: 'list'}, {name: 'v-show', value: 'isShow'}]
这个属性数组最终经过 makeAttrsMap
函数转换为:{':class': 'classObject', 'class': 'list', 'v-show': 'isShow'}
我们还是以概述里面的案例来分析,通过 createASTElement
函数生成最终对象的结构。
<ul :class="classObject" class="list" v-show="isShow">
<li v-for="(l, i) in list" :key="i" @click="clickItem(index)">{{ i }}:{{ l }}</li>
</ul>
2
3
- 其中
ul
节点转换后的值如下:
{
"type":1,
"tag":"ul",
"attrsList":[
{
"name":":class",
"value":"classObject",
"start":4,
"end":24
},{
"name":"class",
"value":"list",
"start":25,
"end":37
},{
"name":"v-show",
"value":"isShow",
"start":38,
"end":53
}
],
"attrsMap":{
":class":"classObject",
"class":"list",
"v-show":"isShow"
},
"rawAttrsMap":{},
"children":[]
}
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
- 其中
li
节点转换后的值如下:
{
"type":1,
"tag":"li",
"attrsList":[
{
"name":"v-for",
"value":"(l, i) in list",
"start":71,
"end":93
},{
"name":":key",
"value":"i",
"start":94,
"end":102
},{
"name":"@click",
"value":"clickItem(index)",
"start":103,
"end":128
}
],
"attrsMap":{
"v-for":"(l, i) in list",
":key":"i",
"@click":"clickItem(index)"
},
"rawAttrsMap":{},
"parent":{
"type":1,
"tag":"ul",
"attrsList":[
{
"name":":class",
"value":"classObject",
"start":4,
"end":24
},{
"name":"class",
"value":"list",
"start":25,
"end":37
},{
"name":"v-show",
"value":"isShow",
"start":38,
"end":53
}
],
"attrsMap":{
":class":"classObject",
"class":"list",
"v-show":"isShow"
},
"rawAttrsMap":{},
"children":[]
},
"children":[]
}
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
# 5. parse
在分析 parse
之前我们先整理一下 parse
的整体结构,如下:
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// 平台化选项变量
// 其他变量
// 警告日志函数
function warnOnce (msg, range) {
/* 省略... */
}
// 关闭节点
function closeElement (element) {
/* 省略... */
}
// 删除尾部空白节点
function trimEndingWhitespace (el) {
/* 省略... */
}
// 校验根节点
function checkRootConstraints (el) {
/* 省略... */
}
parseHTML(template, {
// 其他选项...
start (tag, attrs, unary, start, end) {
/* 省略... */
},
end (tag, start, end) {
/* 省略... */
},
chars (text: string, start: number, end: number) {
/* 省略... */
},
comment (text: string, start, end) {
/* 省略... */
}
})
return root
}
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
# 5.1 变量
在分析标签处理之前我们先看一下一些变量的作用,如下:
源码目录:src/compiler/parser/index.js
// 平台化选项
warn = options.warn || baseWarn
platformIsPreTag = options.isPreTag || no /
platformMustUseProp = options.mustUseProp || no
platformGetTagNamespace = options.getTagNamespace || no
const isReservedTag = options.isReservedTag || no
maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
delimiters = options.delimiters
const preserveWhitespace = options.preserveWhitespace !== false
const whitespaceOption = options.whitespace
// 其他变量
const stack = []
let root
let currentParent
let inVPre = false
let inPre = false
let warned = false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
warn
:用来打印警告信息的platformIsPreTag
:判断标签是否是pre
标签platformMustUseProp
:用来检测一个属性在标签中是否要使用元素对象原生的prop
进行绑定,注意:这里的prop
指的是元素对象的属性,而非Vue
中的props
概念platformGetTagNamespace
:用来获取元素(标签)的命名空间isReservedTag
:判断标签是否是保留的标签maybeComponent
:判断是否为组件transforms
:
我们前面分析过 options
的值,知道 options.module
值如下:
options.modules = [
{
staticKeys: ['staticClass'],
transformNode,
genData
},
{
staticKeys: ['staticStyle'],
transformNode,
genData
},
{
preTransformNode
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我们再来看看 pluckModuleFunction
函数的定义,如下:
源码目录:src/compiler/helpers.js
export function pluckModuleFunction<F: Function> (
modules: ?Array<Object>,
key: string
): Array<F> {
return modules
? modules.map(m => m[key]).filter(_ => _)
: []
}
2
3
4
5
6
7
8
pluckModuleFunction
函数的作用是从第一个参数中"采摘"出函数名字与第二个参数所指定字符串相同的函数,并将它们组成一个数组。
源码中 transforms
的值的获取代码如下:
transforms = pluckModuleFunction(options.modules, 'transformNode')
调用这句代码,我们分两步来看,第一步是 map
,即如下:
options.modules.map(m => m['transformNode'])
所以第一步通过 map
遍历后的值为:
[
transformNode,
transformNode,
undefined
]
2
3
4
5
接着我们继续看第二步,如下:
[
transformNode,
transformNode,
undefined
].filter(_ => _)
2
3
4
5
filter
的作用是过滤掉 undefined
,所以最终得到的 transforms
为,如下:
[
transformNode,
transformNode
]
2
3
4
preTransforms
:同transforms
,所以最终得到的preTransforms
为 :
[ preTransformNode ]
postTransforms
:同transforms
,所以最终得到的postTransforms
为 :
[]
delimiters
:改变纯文本插入分隔符。修改指令的书写风格,比如默认是delimiters: ['${', '}']
之后变成这样${mgs}
preserveWhitespace
:判断是否保留元素之间的空白,用来告诉编译器在编译html
字符串时是否放弃标签之间的空格,如果为true
则代表放弃whitespaceOption
:空白处理策略,'preserve' | 'condense'
stack
:是用来修正当前正在解析元素的父级root
:定义AST
模型对象currentParent
:描述对象之间的父子关系inVPre
:标识当前解析的标签是否在拥有v-pre
的标签之内inPre
:标识当前正在解析的标签是否在<pre></pre>
标签之内warned
:标识只会打印一次警告信息,默认为false
# 5.2 处理标签
说明:关于 parse
中处理标签我们会在 第十二章 (opens new window) 做详细分析。