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) 做详细分析。