vue源码分析(十二) 编译之解析(parse)——处理标签
# 1. 概述
通过我们在第十一章 (opens new window)中的分析,我们可以知道对于构建 AST
来说最关键的选项就是四个钩子函数选项:
- 1、
start
钩子函数,在解析html
字符串时每次遇到 开始标签 时就会调用该函数 - 2、
end
钩子函数,在解析html
字符串时每次遇到 结束标签 时就会调用该函数 - 3、
chars
钩子函数,在解析html
字符串时每次遇到 纯文本 时就会调用该函数 - 4、
comment
钩子函数,在解析html
字符串时每次遇到 注释节点 时就会调用该函数
下面我们就从 start
钩子函数开始说起,为什么从 start
钩子函数开始呢?因为正常情况下,解析一段 html
字符串时必然最先遇到的就是开始标签。所以我们从 start
钩子函数开始讲解,在讲解的过程中为了说明某些问题我们会逐个举例。
# 2. 处理开始标签
# 2.1 整体流程
我们首先来看一下处理开始标签的源码,如下:
源码目录:src/compiler/parser/index.js
start (tag, attrs, unary, start, end) {
// check namespace.
// inherit parent ns if there is one
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
// 1.创建 AST 元素
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
// 2.处理 AST 元素
if (process.env.NODE_ENV !== 'production') {
if (options.outputSourceRange) {
element.start = start
element.end = end
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
cumulated[attr.name] = attr
return cumulated
}, {})
}
attrs.forEach(attr => {
if (invalidAttributeRE.test(attr.name)) {
warn(
`Invalid dynamic argument expression: attribute names cannot contain ` +
`spaces, quotes, <, >, / or =.`,
{
start: attr.start + attr.name.indexOf(`[`),
end: attr.start + attr.name.length
}
)
}
})
}
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true
process.env.NODE_ENV !== 'production' && warn(
'Templates should only be responsible for mapping the state to the ' +
'UI. Avoid placing tags with side-effects in your templates, such as ' +
`<${tag}>` + ', as they will not be parsed.',
{ start: element.start }
)
}
// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
if (!inVPre) {
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
processFor(element)
processIf(element)
processOnce(element)
}
// AST 树管理
if (!root) {
root = element
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(root)
}
}
if (!unary) {
currentParent = element
// 为parse函数,stack标签堆栈添加一个标签
stack.push(element)
} else {
// 关闭节点
closeElement(element)
}
}
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
首先我们从参数开始分析,通过源码我们知道,处理开始标签的函数 start (tag, attrs, unary, start, end)
接收五个参数,分别为:
tag
:标签名称attrs
:标签属性unary
:标签是否是一元标签,如果不是则为真start
:开始end
:结束
接下来我们继续分析函数体,首先执行如下语句:
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
这句代码的作用是获取 标签的命名空间 ,currentParent
为当前元素的父级元素描述对象,如果当前元素存在父级并且父级元素存在命名空间,则使用父级的命名空间作为当前元素的命名空间。 如果父级元素不存在或父级元素没有命名空间,那么会通过调用 platformGetTagNamespace(tag)
函数获取当前元素的命名空间。
注意:platformGetTagNamespace
函数只会获取 svg
和 math
这两个标签的命名空间,但这两个标签的所有子标签都会继承它们两个的命名空间。对于其他标签则不存在命名空间。
继续往下看,接下来执行的如下代码:
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
2
3
4
5
在分析这段代码之前我们先来分析一下 isIE
,代码如下:
源码目录:src/core/util/env.js
export const inBrowser = typeof window !== 'undefined'
export const UA = inBrowser && window.navigator.userAgent.toLowerCase()
export const isIE = UA && /msie|trident/.test(UA)
2
3
isIE
用来判断当前宿主环境是否是 IE
浏览器。
所以上面的if
语句的作用是,如果是 IE
浏览器并且当前元素的命名空间为 svg
,则会调用 guardIESVGBug
函数处理当前元素的属性数组 attrs
,并使用处理后的结果重新赋值给 attrs
变量。这看上去像是在处理 IE
浏览器中关于 svg
标签的 bug
,实际上确实是这样的,大家可以访问 IE 11 bug (opens new window) 了解这个问题的详情,该问题是 svg
标签中渲染多余的属性,如下 svg
标签:
<svg xmlns:feature="http://www.openplans.org/topp"></svg>
被渲染为:
<svg xmlns:NS1="" NS1:xmlns:feature="http://www.openplans.org/topp"></svg>
标签中多了 'xmlns:NS1="" NS1:'
这段字符串,解决办法也很简单,将整个多余的字符串去掉即可。而 guardIESVGBug
函数就是用来修改 NS1:xmlns:feature
属性并移除 xmlns:NS1=""
属性的,如下是 guardIESVGBug
函数的源码以及它需要的两个正则:
const ieNSBug = /^xmlns:NS\d+/
const ieNSPrefix = /^NS\d+:/
/* istanbul ignore next */
function guardIESVGBug (attrs) {
const res = []
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i]
if (!ieNSBug.test(attr.name)) {
attr.name = attr.name.replace(ieNSPrefix, '')
res.push(attr)
}
}
return res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在 guardIESVGBug
函数之前定义了两个正则常量,其中 ieNSBug
正则用来匹配那些以字符串 xmlns:NS
再加一个或多个数字组成的字符串开头的属性名,如:
<svg xmlns:NS1=""></svg>
如上标签的 xmlns:NS1
属性将会被 ieNSBug
正则匹配成功。另外一个正则常量是 ieNSPrefix
,它用来匹配那些以字符串 NS
再加一个或多个数字以及字符 :
所组成的字符串开头的属性名,如:
<svg NS1:xmlns:feature="http://www.openplans.org/topp"></svg>
如上标签的 NS1:xmlns:feature
属性将被 ieNSPrefix
正则匹配成功。
guardIESVGBug
函数接收元素的属性数组作为参数,并返回一个新的数组,新数组与原数组结构相同。可以看到 guardIESVGBug
函数内部通过 for
循环遍历了元素的属性数组,接着使用正则 ieNSBug
去匹配属性名字,可以发现只要不满足 ieNSBug
正则的属性名,都会尝试使用 ieNSPrefix
正则去匹配该属性名并将匹配到的字符替换为空字符串。如下是渲染产生 bug
后的代码:
<svg xmlns:NS1="" NS1:xmlns:feature="http://www.openplans.org/topp"></svg>
在解析如上标签时,传递给 start
钩子函数的标签属性数组 attrs
为:
attrs = [
{
name: 'xmlns:NS1',
value: ''
},
{
name: 'NS1:xmlns:feature',
value: 'http://www.openplans.org/topp'
}
]
2
3
4
5
6
7
8
9
10
在经过 guardIESVGBug
函数处理之后,属性数组中的第一项因为属性名满足 ieNSBug
正则被剔除,第二项属性名字 NS1:xmlns:feature
将被变为 xmlns:feature
,所以 guardIESVGBug
返回的属性数组为:
attrs = [
{
name: 'xmlns:feature',
value: 'http://www.openplans.org/topp'
}
]
2
3
4
5
6
以上就是 guardIESVGBug
函数的作用。
我们继续往下分析,接下来执行的代码是:
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
2
3
4
这段代码的作用是为当前元素创建了描述对象,并将当前标签的元素描述对象赋值给 element
变量。紧接着检查当前元素是否存在命名空间 ns
,如果存在则在元素对象上添加 ns
属性,其值为命名空间的值。
说明:关于 createASTElement
我们在 第十一章 (opens new window) 已经做详细分析。
我们继续往下分析,源码如下:
if (process.env.NODE_ENV !== 'production') {
if (options.outputSourceRange) {
element.start = start
element.end = end
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
cumulated[attr.name] = attr
return cumulated
}, {})
}
attrs.forEach(attr => {
if (invalidAttributeRE.test(attr.name)) {
warn(
`Invalid dynamic argument expression: attribute names cannot contain ` +
`spaces, quotes, <, >, / or =.`,
{
start: attr.start + attr.name.indexOf(`[`),
end: attr.start + attr.name.length
}
)
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
首先判断在开发环境下执行如上代码,outputSourceRange
的作用是判断生产环境还是开发环境,开发环境为 true
。
所以当outputSourceRange
为 true
时,给element 添加start
和 end
属性,分别将参数 start
和 end
赋值给对应的属性。
我们在上一章分析 createASTElement
函数生成的 AST
树为:
{
"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
这里通过 element.attrsList.reduce
,将数组转为对象,如下:
{
:class: {name: ":class", value: "classObject", start: 4, end: 24},
class: {name: "class", value: "list", start: 25, end: 37},
v-show: {name: "v-show", value: "isShow", start: 38, end: 53}
}
2
3
4
5
接下来执行 forEach
循环,通过正则表达式 invalidAttributeRE
判断属性名称中是否包含 空白 或 "
或 '
或 <
或 >
或 /
或 =
字符,如果包含在开发环境下报警告。
我们继续往下分析,接着执行如下代码:
// isForbiddenTag:如果是style或者是是script 标签并且type属性不存在或者存在并且是javascript属性的时候返回真
// isServerRendering:不是在服务器node环境下
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true
process.env.NODE_ENV !== 'production' && warn(
'Templates should only be responsible for mapping the state to the ' +
'UI. Avoid placing tags with side-effects in your templates, such as ' +
`<${tag}>` + ', as they will not be parsed.',
{ start: element.start }
)
}
2
3
4
5
6
7
8
9
10
11
首先我们先来看一下 isForbiddenTag
和 isServerRendering
的定义,如下:
源码目录:src/compiler/parser/index.js
/**
* 是否是禁止在模板中使用的标签
* @param {*} el
*/
function isForbiddenTag (el): boolean {
return (
el.tag === 'style' ||
(el.tag === 'script' && (
!el.attrsMap.type ||
el.attrsMap.type === 'text/javascript'
))
)
}
2
3
4
5
6
7
8
9
10
11
12
13
根据源码可知以下标签为被禁止的标签:
- 1、
<style>
标签为被禁止的标签 - 2、没有指定
type
属性或虽然指定了type
属性但其值为text/javascript
的<script>
标签被认为是被禁止的
源码目录:src/core/util/env.js
// this needs to be lazy-evaled because vue may be required before
// vue-server-renderer can set VUE_ENV
let _isServer
export const isServerRendering = () => {
if (_isServer === undefined) {
/* istanbul ignore if */
if (!inBrowser && !inWeex && typeof global !== 'undefined') {
// detect presence of vue-server-renderer and avoid
// Webpack shimming the process
_isServer = global['process'] && global['process'].env.VUE_ENV === 'server'
} else {
_isServer = false
}
}
return _isServer
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
通过分析上面的两个函数,我们知道 if
语句的作用是如果当前标签是被禁止的,并且在非服务端渲染的情况下,会打印警告信息,同时还会在当前元素的描述对象上添加 el.forbidden
属性,并将其值设置为 true
。
我们继续往下看代码,接下来要执行的是如下这段代码:
// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
2
3
4
如上代码中使用 for
循环遍历了 preTransforms
数组,我们知道 preTransforms
是通过 pluckModuleFunction
函数从 options.modules
选项中筛选出名字为 preTransformNode
函数所组成的数组。该数组中每个元素都是一个函数,所以如上代码的 for
循环内部直接调用了 preTransforms
数组中的每一个函数并为这些函数传递了两个参数,分别是当前元素描述对象(element
)以及编译器选项(options
)。
说明:关于 preTransforms[i](element, options)
调用的 preTransformNode
函数,我们会在后面小节做详细分析。
我们继续往下分析,接下来执行如下这段代码:
if (!inVPre) {
// 检查标签是否有v-pre 指令,含有 v-pre 指令的标签里面的指令则不会被编译
processPre(element)
if (element.pre) { // 标签是否含有 v-pre 指令
inVPre = true // 如果标签有v-pre 指令,则标记为true
}
}
// 判断标签是否是pre 如果是则返回真
if (platformIsPreTag(element.tag)) {
inPre = true
}
// v-pre 指令存在
if (inVPre) {
// 浅拷贝属性把虚拟dom的attrsList拷贝到attrs中,如果没有pre块,标记plain为true
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
// 判断获取v-for属性是否存在如果有则转义 v-for指令,把for,alias,iterator1,iterator2属性添加到虚拟dom中
processFor(element)
// 获取v-if属性,为el虚拟dom添加 v-if,v-eles,v-else-if 属性
processIf(element)
// 获取v-once 指令属性,如果有有该属性,为虚拟dom标记事件只触发一次则销毁
processOnce(element)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这段代码的作用是对当前元素描述对象做额外的处理,使得该元素描述对象能更好的描述一个标签。简单点说就是在元素描述对象上添加各种各样的具有标识作用的属性,就比如之前遇到的 ns
属性和 forbidden
属性,它们都能够对标签起到描述作用。
说明:关于 processxxx
这类函数,我们会在后面小节做详细分析。
我们继续往下分析,接下来执行的是如下代码:
// 根节点不存在
if (!root) {
root = element
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(root)
}
}
2
3
4
5
6
7
首先我们看一下 checkRootConstraints
函数的定义:
function checkRootConstraints (el) {
if (el.tag === 'slot' || el.tag === 'template') { // 根节点不能为 slot 或 template 标签
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.',
{ start: el.start }
)
}
if (el.attrsMap.hasOwnProperty('v-for')) { // 根节点不能有 v-for 指令
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.',
el.rawAttrsMap['v-for']
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
checkRootConstraints
作用是用来检测模板根元素是否符合要,不能使用 slot
和 template
标签作为模板根元素,这是因为 slot
作为插槽,它的内容是由外界决定的,而插槽的内容很有可能渲染多个节点,template
元素的内容虽然不是由外界决定的,但它本身作为抽象组件是不会渲染任何内容到页面的,而其又可能包含多个子节点,所以也不允许使用 template
标签作为根节点。总之这些限制都是出于 必须有且仅有一个根元素 考虑的。
所有这段代码的作用是判断 root
是否存在,如果 root
不存在那说明当前元素应该就是根元素,所以在 if
语句块内直接将当前元素的描述对象 element
赋值给 root
变量,同时在开发环境中会调用上面刚刚讲过的 checkRootConstraints
函数检查根元素是否符合要求。
我们继续往下分析,接下来执行的是如下代码:
if (!unary) {
currentParent = element
// 为parse函数,stack标签堆栈添加一个标签
stack.push(element)
} else {
// 关闭节点
closeElement(element)
}
2
3
4
5
6
7
8
如上这段代码是一个 if...else
条件分支语句块,我们首先看 if
语句的条件,它检测了当前元素是否是非一元标签,前面我们说过了如果一个元素是非一元标签,那么应该将该元素的描述对象添加到 stack
栈中,并且将 currentParent
变量的值更新为当前元素的描述对象,如上代码中 if
语句块内的代码说明了一切。
反之,如果一个元素是一元标签,那么应该调用 closeElement
函数闭合该元素。对于 closeElement
函数我们后面再详细说,现在我们需要重点关注 if
语句块内的两句代码,通过这两句代码我们至少能得到一个总结:每当遇到一个非一元标签都会将该元素的描述对象添加到 stack
数组,并且 currentParent
始终存储的是 stack
栈顶的元素,即当前解析元素的父级。
到目前为止,我们大概粗略地过了一遍 start
钩子函数的内容,接下来我们做一些总结,以使得我们的思路更加清晰:
- 1、
start
钩子函数是当解析html
字符串遇到开始标签时被调用的。 - 2、模板中禁止使用
<style>
标签和那些没有指定type
属性或type
属性值为text/javascript
的<script>
标签。 - 3、在
start
钩子函数中会调用前置处理函数,这些前置处理函数都放在preTransforms
数组中,这么做的目的是为不同平台提供对应平台下的解析工作。 - 4、前置处理函数执行完之后会调用一系列
process*
函数继续对元素描述对象进行加工。 - 5、通过判断
root
是否存在来判断当前解析的元素是否为根元素。 - 6、
slot
标签和template
标签不能作为根元素,并且根元素不能使用v-for
指令。 - 7、可以定义多个根元素,但必须使用
v-if
、v-else-if
以及v-else
保证有且仅有一个根元素被渲染。 - 8、构建
AST
并建立父子级关系是在start
钩子函数中完成的,每当遇到非一元标签,会把它存到currentParent
变量中,当解析该标签的子节点时通过访问currentParent
变量获取父级元素。 - 9、如果一个元素使用了
v-else-if
或v-else
指令,则该元素不会作为子节点,而是会被添加到相符的使用了v-if
指令的元素描述对象的ifConditions
数组中。 - 10、如果一个元素使用了
slot-scope
特性,则该元素也不会作为子节点,它会被添加到父级元素描述对象的scopedSlots
属性中。 - 11、对于没有使用条件指令或
slot-scope
特性的元素,会正常建立父子级关系。
# 2.2 processPre
processPre
的作用是处理使用了v-pre
指令的元素及其子元素,我们来看一下源码:
源码目录:src/compiler/parser/index.js
if (!inVPre) {
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
2
3
4
5
6
7
8
9
10
我们在前面分析变量的时候已经分析过了 inVPre
的作用是标识当前解析的标签是否在拥有 v-pre
的标签之内,所以if (!inVPre)
,该条件保证了如果当前解析工作已经处于 v-pre
环境下了,则不需要再次执行该 if
语句块内的代码。
接下来我们详细看一下 processPre
的定义:
源码目录:src/compiler/parser/index.js
function processPre (el) {
if (getAndRemoveAttr(el, 'v-pre') != null) {
el.pre = true
}
}
2
3
4
5
通过查看源码,我们知道 processPre
函数接收元素描述对象作为参数,在 processPre
函数内部首先通过 getAndRemoveAttr
函数并使用其返回值与 null
做比较,如果 getAndRemoveAttr
函数的返回值不等于 null
则执行 if
语句块内的代码,即在元素描述对象上添加 .pre
属性并将其值设置为 true
。
我们再来看看 getAndRemoveAttr
的定义:
源码目录:src/compiler/helpers.js
export function getAndRemoveAttr (
el: ASTElement,
name: string,
removeFromMap?: boolean
): ?string {
let val
// el.attrsMap,例如 attrsMap: {ref: "child"}
if ((val = el.attrsMap[name]) != null) {
// attrsList: [{name: "ref", value: "child", start: 76, end: 87}]
const list = el.attrsList
for (let i = 0, l = list.length; i < l; i++) {
if (list[i].name === name) {
list.splice(i, 1) // 删除属性
break
}
}
}
if (removeFromMap) {
delete el.attrsMap[name]
}
return val
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
首先 getAndRemoveAttr
函数接收三个参数分别为:
el
:AST
树name
:属性名称removeFromMap
:是否要删除属性的标志
在分析这个函数的具体作用时,我们还是已前面的例子来进行分析说明,如下:
{
"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
通过上面的例子我们知道,el.attrsMap
和 el.attrsList
分别为:
"attrsMap":{
":class":"classObject",
"class":"list",
"v-show":"isShow"
}
"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 }
]
2
3
4
5
6
7
8
9
10
11
所以我们已 getAndRemoveAttr(el, 'v-show', true)
为例,通过执行 val = el.attrsMap[name]
获取属性值,此时 val
的值为 isShow
,不为空,接着执行 if
里面的语句,通过 const list = el.attrsList
将属性数组赋值给 list
然后通过 for
循环遍历 list
数组,通过 list[i].name === name
判断属性 v-show
是否存在,如果存在则删除 list
中的 name为 v-show
对应的数据项 ,并且跳出循环,此时 attrsList
变为如下值:
"attrsList":[
{ "name":":class", "value":"classObject", "start":4, "end":24 },
{ "name":"class", "value":"list", "start":25, "end":37 }
]
2
3
4
接下来通过 if (removeFromMap)
判断第三个参数是否为真,如果为真则执行 if
语句,否则跳过 if
语句,在我们当前的案例中,removeFromMap = true
所以执行 delete el.attrsMap[name]
,删除 el.attrsMap
中的 v-show
属性,此时 attrsMap
变为如下值:
"attrsMap":{
":class":"classObject",
"class":"list"
}
2
3
4
最后返回 val
,如果 val
不存在则返回 undefined
,在此案例中返回 isShow
。
到此为止,我们终于知道了 processPre
的作用是获取给定元素 v-pre
属性的值,如果 v-pre
属性的值不等于 null
则会在元素描述对象上添加 .pre
属性,并将其值设置为 true
我们再回到 start
函数,执行完 ,继续执行下列语句:
if (element.pre) {
inVPre = true
}
2
3
此段代码判断了元素对象的 .pre
属性是否为真,我们知道假如一个标签使用了 v-pre
指令,那么经过 processPre
函数处理之后,该元素描述对象的 .pre
属性值为 true
,这时会将 inVPre
变量的值也设置为 true
。当 inVPre
变量为真时,意味着 后续的所有解析工作都处于 v-pre
环境下,编译器会跳过拥有 v-pre
指令元素以及其子元素的编译过程,所以后续的编译逻辑需要 inVPre
变量作为标识才行。
我们继续往下分析,接下来执行如下代码:
if (platformIsPreTag(element.tag)) {
inPre = true
}
2
3
这段代码的作用是使用 platformIsPreTag
函数判断当前元素是否是 <pre>
标签,如果是 <pre>
标签则将 inPre
变量设置为 true
。实际上 inPre
变量与 inVPre
变量的作用相同,都是用来作为一个标识,只不过 inPre
变量标识着当前解析环境是否在 <pre>
标签内,因为 <pre>
标签内的解析行为与其他 html
标签是不同。具体不同体现在:
<pre>
标签会对其所包含的html
字符实体进行解码<pre>
标签会保留html
字符串编写时的空白
# 2.3 processRawAttrs
我们继续往下分析,当 inVPre
变量的值也设置为 true
会执行 processRawAttrs
函数,代码如下:
源码目录:src/compiler/parser/index.js
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
/* 省略 */
}
2
3
4
5
在分析 processRawAttrs
之前,我们还是已下面的案例来分析,如下:
<div v-pre v-on:click="handleClick"></div>
此段代码转换为 AST
为,如下:
{
"type":1,
"tag":"div",
"attrsList":[
{"name": "v-pre", "value": "", "start": 5, "end": 10},
{"name": "v-on:click", "value": "handleClick", "start": 11, "end": 35}
],
"attrsMap":{
"v-on:click": "handleClick"
"v-pre": ""
},
"rawAttrsMap":{},
"children":[],
"parent": undefined
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
通过上一小节的分析,此处 div
标签拥有 v-pre
指令,所以 inVPre
为 true
,此时会执行 processRawAttrs(element)
,我们继续来看 processRawAttrs
的定义,如下:
源码目录:src/compiler/parser/index.js
function processRawAttrs (el) {
const list = el.attrsList
const len = list.length
if (len) {
const attrs: Array<ASTAttr> = 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) {
// non root node in pre blocks with no attributes
el.plain = true
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
通过上一小节的分析,我们知道,经过 processPre
处理后的 AST
树为:
{
"type":1,
"tag":"div",
"attrsList":[
{"name": "v-on:click", "value": "handleClick", "start": 11, "end": 35}
],
"attrsMap":{
"v-on:click": "handleClick"
"v-pre": ""
},
"rawAttrsMap":{
"v-pre": {"name": "v-pre", "value": "", "start": 5, "end": 10},
"v-on:click": {"name": "v-on:click", "value": "handleClick", "start": 11, "end": 35}
},
"children":[],
"parent": undefined,
"start": 0,
"end": 36,
"pre": true
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
所以此时 processRawAttrs
接收到的 el
为上面的 AST
树,首先通过执行如下代码:
const list = el.attrsList
const len = list.length
2
获取 attrsList
属性和attrsList
数组的长度,在此案例中attrsList.length
的值为 1, attrsList
值为:
"attrsList":[
{"name": "v-on:click", "value": "handleClick", "start": 11, "end": 35}
]
2
3
接下来通过 if (len)
判读 attrsList.length
的值,此处为1,所以执行 if
语句,接着通过 const attrs: Array<ASTAttr> = el.attrs = new Array(len)
创建一个长度等于 attrsList.length
的数组并给 el
添加 attrs
值为新创建的数组。然后通过 for
循环给 attrs
添加 name
、value
、start
、end
属性。
注意这里value
的值使用 JSON.stringify
实际上就是保证最终生成的代码中 el.attrsList[i].value
属性始终被作为普通的字符串处理。通过以上代码的讲解我们知道了,如果一个标签的解析处于 v-pre
环境,则会将该标签的属性全部添加到元素描述对象的 .attrs
数组中,并且 .attrs
数组与 .attrsList
数组几乎相同,唯一不同的是在 .attrs
数组中每个对象的 value
属性值都是通过 JSON.stringify
处理过的。
假如 el.attrsList
数组的长度为 0
,则会进入 else...if
分支的判断,检查该元素是否使用了 v-pre
指令,如果没有使用 v-pre
指令才会执行 else...if
语句块的代码。思考一下,首先我们有一个大前提,即 processRawAttrs
函数的执行说明当前解析必然处于 v-pre
环境,要么是使用 v-pre
指令的标签自身,要么就是其子节点。同时 el.attrsList
数组的长度为 0
说明该元素没有任何属性,而且 else...if
条件的成立也说明该元素没有使用 v-pre
指令,这说明该元素一定是使用了 v-pre
指令的标签的子标签,如下:
<div v-pre>
<span></span>
</div>
2
3
如上 html
字符串所示,当解析 span
标签时,由于 span
标签没有任何属性,并且 span
标签也没有使用 v-pre
指令,所以此时会在 span
标签的元素描述对象上添加 .plain
属性并将其设置为 true
,用来标识该元素是纯的,在代码生成的部分我们将看到一个被标识为 plain
的元素将有哪些不同。
最后我们对使用了 v-pre
指令的标签所生成的元素描述对象做一个总结:
- 如果标签使用了
v-pre
指令,则该标签的元素描述对象的element.pre
属性将为true
。 - 对于使用了
v-pre
指令的标签及其子代标签,它们的任何属性都将会被作为原始属性处理,即使用processRawAttrs
函数处理之。 - 经过
processRawAttrs
函数的处理,会在元素的描述对象上添加element.attrs
属性,它与element.attrsList
数组结构相同,不同的是element.attrs
数组中每个对象的value
值会经过JSON.stringify
函数处理。 - 如果一个标签没有任何属性,并且该标签是使用了
v-pre
指令标签的子代标签,那么该标签的元素描述对象将被添加element.plain
属性,并且其值为true
。
# 2.4 processFor
在分析 processFor
我们还是已前面的案例来进行讲解,代码如下:
<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
我们看一下 处理 v-for
的代码,如下:
源码目录:src/compiler/parser/index.js
if (inVPre) {
/* 省略 */
} else if (!element.processed) {
// structural directives
processFor(element)
/* 省略 */
}
2
3
4
5
6
7
首先通过判断 element.processed
的值,如果为 false
,则执行 if
语句,其中element.processed
属性是一个布尔值,它标识着当前元素是否已经被解析过,接下来我们看一下 processFor
的定义,如下:
源码目录:src/compiler/parser/index.js
export function processFor (el: ASTElement) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
const res = parseFor(exp)
if (res) {
extend(el, res)
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid v-for expression: ${exp}`,
el.rawAttrsMap['v-for']
)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
processFor
中首先通过 getAndRemoveAttr
移除 element.attrsList
对象中 name
为 v-for
的属性,并且返回获取到 v-for
属性的值赋值给变量 exp
,例如 v-for="(l, i) in list"
获取到的 exp
为 (l, i) in list
。
接下来通过 parseFor
对 v-for
属性的值做解析,源码如下:
export function parseFor (exp: string): ?ForParseResult {
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
const res = {}
res.for = inMatch[2].trim()
const alias = inMatch[1].trim().replace(stripParensRE, '')
const iteratorMatch = alias.match(forIteratorRE)
if (iteratorMatch) {
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
parseFor
一开始通过 forAliasRE
正则表达式去捕获字符串,此案例中 exp
为 (l, i) in list
所以执行 exp.match(forAliasRE)
得到的 inMatch
为:
inMatch = ["(l, i) in list", "(l, i)", "list"]
说明:关于 forAliasRE
我们已经在前面章节中做了详细分析,请移步到 这里 (opens new window) 学习。
如果匹配失败则直接返回 undefined
,如果匹配成功,继续执行后续代码,首先定义一个 res
空对象,接着给 res
添加 for
,此案例中值为 inMatch[2]
即 "list"
,接下来 获取到 inMatch[1]
即 "(l, i)"
,通过 trim
去掉空格再通过 stripParensRE
去掉左右圆括号即 alias = 'l, i'
v-for
指令的值与 alias
常量值的对应关系:
- 如果
v-for
指令的值为'l in list'
,则alias
的值为字符串'l'
- 如果
v-for
指令的值为'(l, i) in list'
,则alias
的值为字符串'l, i'
- 如果
v-for
指令的值为'(l, k, i) in list'
,则alias
的值为字符串'l, k, i'
说明:关于 stripParensRE
我们已经在前面章节中做了详细分析,请移步到 这里 (opens new window) 学习。
接下来,执行如下语句:
const iteratorMatch = alias.match(forIteratorRE)
这里定义了 iteratorMatch
常量,它的值是使用 alias
字符串的 match
方法匹配正则 forIteratorRE
得到的,其中正则 forIteratorRE
我们也已经在前面的章节中讲过了,这里总结一下对于不同的 alias
字符串其对应的匹配结果:
- 如果
alias
字符串的值为'l'
,则匹配结果iteratorMatch
常量的值为null
- 如果
alias
字符串的值为'l, i'
,则匹配结果iteratorMatch
常量的值是一个包含两个元素的数组:[', i', 'i', undefined]
- 如果
alias
字符串的值为'l, k, i'
,则匹配结果iteratorMatch
常量的值是一个包含三个元素的数组:[', k, i', 'k', 'i']
所以在此案例中 alias.match(forIteratorRE)
匹配到的值为 [',i', 'i', undefined]
,此时 iteratorMatch
不为空,所以执行 if
语句,如下:
res.alias = alias.replace(forIteratorRE, '').trim()
res.iterator1 = iteratorMatch[1].trim()
if (iteratorMatch[2]) {
res.iterator2 = iteratorMatch[2].trim()
}
2
3
4
5
首先执行 alias.replace(forIteratorRE, '').trim()
此时的 alias
为 'l, i'
,所以通过 forIteratorRE
匹配 ,i
并替换为空,即 res.alias = 'l'
。通过上面分析我们知道此时 iteratorMatch
值为 [',i', 'i', undefined]
,所以 res.iterator1 = 'i'
,iteratorMatch[2]
为 undefined
即不会执行 if
语句。
最终 res
为,如下对象:
res = {
alias: "l",
for: "list",
iterator1: "i"
}
2
3
4
5
那么什么时候会执行到 if
语句呢?
要执行 if
语句 我们需要对此案例做一点修改,即修改 v-for
的内容,如下:
<ul :class="classObject" class="list" v-show="isShow">
<li v-for="(l, k, i) in list" :key="i" @click="clickItem(index)">{{ i }}:{{ l }}</li>
</ul>
2
3
此时我们获取到的 iteratorMatch
值为 [',k ,i', 'k', 'i']
,所以 iteratorMatch[2]
为 i
,此时 res
为,如下对象:
res = {
alias: "l"
for: "list",
iterator1: "k",
iterator2: "i"
}
2
3
4
5
6
最后当为空时,执行 else
语句,如下:
if (iteratorMatch) {
/* 省略 */
} else {
res.alias = alias
}
2
3
4
5
什么时候 iteratorMatch
为空呢?上面我们在分析正则表达式 forIteratorRE
时知道,当案例时如下格式时,forIteratorRE
为空:
<ul :class="classObject" class="list" v-show="isShow">
<li v-for="l in list" :key="i" @click="clickItem(index)">{{ l }}</li>
</ul>
2
3
此时执行 else
语句,所以最终 res
为,如下对象:
res = {
alias: "l"
for: "list"
}
2
3
4
说明:关于 forIteratorRE
我们已经在前面章节中做了详细分析,请移步到 这里 (opens new window) 学习。
至此, parseFor
我们已经分析完了。
我们继续回到 processFor
,在此案例中,通过 const res = parseFor(exp)
解析完 v-for
得到的结果如下:
const res = {
alias: "l",
for: "list",
iterator1: "i"
}
2
3
4
5
继续往下看,代码如下:
if (res) {
extend(el, res)
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid v-for expression: ${exp}`,
el.rawAttrsMap['v-for']
)
}
2
3
4
5
6
7
8
首先我们看看 else if
语句,在分析 parseFor
时我们知道执行 const inMatch = exp.match(forAliasRE)
这句匹配 v-for
的值,如果匹配不到直接返回 undefined
,所以此时会执行 else if
语句,在开发环境中报警告,提示开发者所编写的 v-for
指令的值为无效的。
最后我们看一下 if
语句,可以看到如果 parseFor
函数对 v-for
指令的值解析成功,则会将解析结果保存在 res
常量中,并使用 extend
函数将 res
常量中的属性混入当前元素的描述对象中。
# 2.5 processIf
我们继续往看一下 ,接下来是处理 v-if
的代码,如下:
源码目录:src/compiler/parser/index.js
if (inVPre) {
/* 省略 */
} else if (!element.processed) {
/* 省略 */
processIf(element)
/* 省略 */
}
2
3
4
5
6
7
processIf
是用来处理条件指令的标签的元素描述对象,源码如下:
源码目录:src/compiler/parser/index.js
function processIf (el) {
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
el.if = exp
addIfCondition(el, {
exp: 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
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在分析 processIf
之前我们还是用一个案例来说明,代码如下:
<div>
<div v-if="child === 1"> if </div>
<div v-else-if="child === 2"> else if </div>
<div v-else> else </div>
</div>
2
3
4
5
我们首先以 <div v-if="child === 1"> if </div>
为例,通过 createASTElement
生成的 AST
树为:
{
"type":1,
"tag":"div",
"attrsList":[
{"name":"v-if","value":"child === 1","start":23,"end":41}
],
"attrsMap":{"v-if":"child === 1"},
"rawAttrsMap":{
"v-if":{"name":"v-if","value":"child === 1","start":23,"end":41}
},
"parent":{
"type":1,
"tag":"div",
"attrsList":[],
"attrsMap":{},
"rawAttrsMap":{},
"children":[],
"start":0,
"end":5
},
"children":[],
"start":18,
"end":42
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
我们继续回到 processIf
中,一开始执行如下语句:
const exp = getAndRemoveAttr(el, 'v-if')
getAndRemoveAttr
我们前面已经分析过了,在这里的作用是移除 name
为 v-if
的属性,并且返回获取到 v-if
属性的值,例如 v-if="child === 1"
获取到的 exp
为 child === 1
。在我们这个案例中 exp
有值,继续执行 if
语句,如下:
if (exp) {
el.if = exp
addIfCondition(el, {
exp: exp,
block: el
})
} else {
/* 省略*/
}
2
3
4
5
6
7
8
9
首先在元素描述对象上定义了 el.if
属性,并且该属性的值就是 v-if
指令的属性值即 el.if = 'child === 1'
。接下来调用 addIfCondition
函数,第一个参数就是当前元素描述对象本身;第二个参数是一个对象,包含两个属性 exp
和 block
值分别为 exp
和 el
,我们再来看看addIfCondition
的定义,如下:
export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
if (!el.ifConditions) {
el.ifConditions = []
}
el.ifConditions.push(condition)
}
2
3
4
5
6
此处 addIfCondition
注意住了两件事:
- 在元素描述对象上定义了
el.ifConditions
属性 - 将条件对象添 即形如
{exp: exp, block: el}
这样的对象,加到el.ifConditions
属性的数组中
我们再回到 processIf
函数中,当 v-if
匹配不到会执行 else
语句,如下:
if (exp) {
/* 省略 */
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
2
3
4
5
6
7
8
9
10
11
else
的处理逻辑和处理 v-if
时类似,这里主要是对 v-else
和 v-else-if
做处理,即:
- 移除
name
为v-else
的属性,并且返回空字符串即''
,并在元素描述对象上定义了el.else
属性,并且该属性的值就是true
- 移除
name
为v-else-if
的属性,并且返回获取到v-else-if
属性的值,例如v-else-if="child === 2"
获取到的elseif
为child === 2
,然后在元素描述对象上定义了el.elseif
属性,并且该属性的值就是v-elseif
指令的属性值
注意:
对于使用了 v-else
和 v-else-if
这两个条件指令的标签,经过 processIf
函数的处理之后仅仅是在元素描述对象上添加了 el.else
属性和 el.elseif
属性,并没有做额外的工作。 在 processIfConditions
中,当一个元素描述对象存在 el.else
属性或 el.elseif
属性时,该元素描述对象不会作为 AST
中的一个普通节点,而是会被添加到与之相符的带有 v-if
指令的元素描述对象的 ifConditions
数组中。
对于 processIfConditions
我们后面会详细分析。
# 2.6 processOnce
我们继续往下看 ,接下来是处理 v-once
的代码,如下:
源码目录:src/compiler/parser/index.js
if (inVPre) {
/* 省略 */
} else if (!element.processed) {
/* 省略 */
processOnce(element)
}
2
3
4
5
6
在分析 processOnce
之前我们还是用一个案例来说明,代码如下:
<div>
<div v-once> v-once </div>
</div>
2
3
我们首先以 <div v-once> v-once </div>
为例,通过 createASTElement
生成的 AST
树为:
{
"type":1,
"tag":"div",
"attrsList":[
{name: "v-once", value: "", start: 23, end: 29}
],
"attrsMap":{"v-once":""},
"rawAttrsMap":{
"v-once":{name: "v-once", value: "", start: 23, end: 29}
},
"parent":{
"type":1,
"tag":"div",
"attrsList":[],
"attrsMap":{},
"rawAttrsMap":{},
"children":[],
"start":0,
"end":5
},
"children":[],
"start":18,
"end":30
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
我们看一下processOnce
的定义,如下:
function processOnce (el) {
var once = getAndRemoveAttr(el, 'v-once');
if (once != null) {
el.once = true;
}
}
2
3
4
5
6
processOnce
的作用是移除 name
为 v-once
的属性,并且返回空字符串即 ''
,并在元素描述对象上定义了 el.once
属性,并且该属性的值就是 true
。
# 2.7 closeElement
接下来我们分析一下 closeElement
函数,首先看一下如下代码:
源码目录:src/compiler/parser/index.js
function closeElement (element) {
trimEndingWhitespace(element)
/* 省略... */
}
2
3
4
在分析 closeElement
具体的执行逻辑之前,我们函数以一个案例进行分析,如下:
<div>
<input type="text" v-model="val">
</div>
2
3
此案例,通过 createASTElement
生成的 AST
树为:
{
attrsList: [
{name: "type", value: "text", start: 25, end: 36},
{name: "v-model", value: "val", start: 37, end: 50}
],
attrsMap: {type: "text", v-model: "val"},
children: [],
end: 51,
parent: {
attrsList: [],
attrsMap: {},
children: [],
end: 5,
parent: undefined,
rawAttrsMap: {},
start: 0,
tag: "div",
type: 1
}
rawAttrsMap: {
type: {name: "type", value: "text", start: 25, end: 36},
v-model: {name: "v-model", value: "val", start: 37, end: 50}
}
start: 18
tag: "input"
type: 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
closeElement
函数一开始就调用了 trimEndingWhitespace(element)
参数是 AST
树,我们先看一下 trimEndingWhitespace
的定义,如下:
function trimEndingWhitespace (el) {
// remove trailing whitespace node
if (!inPre) {
let lastNode
while (
(lastNode = el.children[el.children.length - 1]) &&
lastNode.type === 3 &&
lastNode.text === ' '
) {
el.children.pop()
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
此案例中 inPre
为 false
,所以继续执行 if
语句,while
循环的成立条件是:
children
存在- 节点类型为文本
- 节点内容为空字符
当上面三个条件都成立的时,从 children
的数组尾部删除最后一个空节点。
说明:nodeType的12种类
const unsigned short ELEMENT_NODE = 1; 元素节点
const unsigned short ATTRIBUTE_NODE = 2; 属性节点
const unsigned short TEXT_NODE = 3; 文本节点
const unsigned short CDATA_SECTION_NODE = 4; CDATA 区段
const unsigned short ENTITY_REFERENCE_NODE = 5; 实体引用元素
const unsigned short ENTITY_NODE = 6; 实体
const unsigned short PROCESSING_INSTRUCTION_NODE = 7; 表示处理指令
const unsigned short COMMENT_NODE = 8; 注释节点
const unsigned short DOCUMENT_NODE = 9; 最外层的Root element,包括所有其它节点
const unsigned short DOCUMENT_TYPE_NODE = 10; <!DOCTYPE………..>
const unsigned short DOCUMENT_FRAGMENT_NODE = 11; 文档碎片节点
const unsigned short NOTATION_NODE = 12; DTD 中声明的符号节点
2
3
4
5
6
7
8
9
10
11
12
接下来执行,如下代码:
if (!inVPre && !element.processed) {
element = processElement(element, options)
}
2
3
此时 input
标签的处理 inVPre
为 false
,processed
表示是否已经处理过,此时也是 false
,所以继续执行 processElement
函数。
说明:关于 processElement
我们会在后面小节做详细分析。
我们继续往下看,代码如下:
// tree management
if (!stack.length && element !== root) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(element)
}
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (process.env.NODE_ENV !== 'production') {
warnOnce(
`Component template should contain exactly one root element. ` +
`If you are using v-if on multiple elements, ` +
`use v-else-if to chain them instead.`,
{ start: element.start }
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
它检测了 stack.length
是否为 0
,也就是说 stack
数组为空的情况下会执行 if
语句块内的代码。我们想一下如果 stack
数组为空并且当前正在解析开始标签,这说明什么问题?要想知道这个问题我们首先要知道 stack
数组的作用,前面已经多次提到每当遇到一个非一元标签时就会将该标签的描述对象放进数组,并且每当遇到一个结束标签时都会将该标签的描述对象从 stack
数组中拿掉,那也就是说在只有一个根元素的情况下,正常解析完成一段 html
代码后 stack
数组应该为空,或者换个说法,即当 stack
数组被清空后则说明整个模板字符串已经解析完毕了,但此时 start
钩子函数仍然被调用了,这说明模板中存在多个根元素,这时 if
语句块内的代码将被执行。
我们知道在编写 Vue
模板时的约束是必须有且仅有一个被渲染的根元素,但你可以定义多个根元素,只要能够保证最终只渲染其中一个元素即可,能够达到这个目的的方式只有一种,那就是在多个根元素之间使用 v-if
或 v-else-if
或 v-else
。
在分析 processIf
时我们知道如果发现元素的属性中有 v-if
或 v-else-if
或 v-else
,会在元素描述对象上添加相应的属性作为标识即 .if
属性、.elseif
属性以及 .else
属性。
首先 root.if
必须为真,要知道一点,即无论定义多少个根元素,root
变量始终存储的是第一个根元素的描述对象,所以 root.if
为真就保证了第一个定义的根元素是使用了 v-if
指令的。同时条件 (element.elseif || element.else)
也必须为真,注意这里是 element.elseif
或 element.else
,而不是 root.elseif
或 root.else
。root
为第一个根元素的描述对象,element
为当前元素描述对象,即非第一个根元素的描述对象。如果以上条件成立就能够保证所有根元素都是由 v-if
、v-else-if
、v-else
等指令控制的,这就间接保证了被渲染的根元素只有一个,此时 if
语句块内的代码将被执行。
在 if
语句块内首先使用 checkRootConstraints
函数检查当前元素是否符合作为根元素的要求,接着调用了 addIfCondition
函数。
addIfCondition
我们在上一小节有作详细说明,这里就不再重复了,但有一点需要强调一下,在上一节我们跟节点只有一个,在这里我们用一个多个跟节点的案例分析这段代码,如下:
<div v-if="child === 1"> if </div>
<div v-else-if="child === 2"> else if </div>
<div v-else> else </div>
2
3
解析后生成的 AST
如下(简化版):
{
type: 1,
tag: 'div',
ifConditions: [
{
exp: 'child === 1',
block: { type: 1, tag: 'div' /* 省略其他属性 */ }
},
{
exp: 'child === 2',
block: { type: 1, tag: 'div' /* 省略其他属性 */ }
},
{
exp: undefined,
block: { type: 1, tag: 'div' /* 省略其他属性 */ }
}
]
// 省略其他属性...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我们继续往下看,当前元素不满足条件:root.if && (element.elseif || element.else)
,那么在非生产环境下 elseif
语句块的代码将会被执行,通过 warnOnce
函数打印了警告信息给开发者友好的提示。
接下来执行如下代码:
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else {
if (element.slotScope) {
// scoped slot
// keep it in the children list so that v-else(-if) conditions can
// find it as the prev node.
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
}
currentParent.children.push(element)
element.parent = currentParent
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
当前元素存在父级(currentParent
),并且当前元素不是被禁止的元素。只有在这种情况下才会执行该 if
条件语句块内的代码。紧接着又是一个 if
语句判断 element.elseif || element.else
,如果当前元素使用了 v-else-if
或 v-else
指令,则会调用 processIfConditions
函数。
如果当前元素没有使用了 v-else-if
或 v-else
指令,则执行 else
语句。else
语句中又是一个条件判断 element.slotScope
即当前元素是否使用了 slot-scope
特性。
如果一个元素使用了 slot-scope
特性,那么该元素的描述对象会被添加到父级元素的 scopedSlots
对象下,也就是说使用了 slot-scope
特性的元素与使用了 v-else-if
或 v-else
指令的元素一样,他们都不会作为父级元素的子节点,对于使用了 slot-scope
特性的元素来讲它们将被添加到父级元素描述对象的 scopedSlots
对象下。
接下来会把当前元素描述对象添加到父级元素描述对象(currentParent
)的 children
数组中,同时将当前元素对象的 parent
属性指向父级元素对象,这样就建立了元素描述对象间的父子级关系。
注意:对于 processIfConditions
我们会在下一小节详细分析。
我们继续往下看,代码如下:
// final children cleanup
// filter out scoped slots
element.children = element.children.filter(c => !(c: any).slotScope)
// remove trailing whitespace node again
trimEndingWhitespace(element)
2
3
4
5
这段代码的主要作用是:
- 过滤掉作用域插槽
- 删除尾部空白节点
接下来我们看一下最后一段代码,如下:
// check pre state
if (element.pre) {
inVPre = false
}
if (platformIsPreTag(element.tag)) {
inPre = false
}
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
2
3
4
5
6
7
8
9
10
11
这段代码的主要作用是:
- 如果标签有
pre
属性,inVPre
设置为false
- 判断标签是否是pre 如果是则返回真
- 使用一个
for
循环遍历了postTransforms
数组,并执行数组中的postTransformNode
函数
说明:只有当遇到二元标签的结束标签或一元标签时才会调用 closeElement
函数。
说明:此处 postTransforms
为空没有 postTransformNode
函数,具体可以移步到 这里 (opens new window) 学习。
# 2.8 processIfConditions
在上一下节中我们知道 processIfConditions
函数,同时将当前元素描述对象 element
和父级元素的描述对象 currentParent
作为参数传递,我们来看看 processIfConditions
函数的定义,如下:
function processIfConditions (el, parent) {
const prev = findPrevElement(parent.children)
if (prev && prev.if) {
addIfCondition(prev, {
exp: el.elseif,
block: el
})
} else if (process.env.NODE_ENV !== 'production') {
warn(
`v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +
`used on element <${el.tag}> without corresponding v-if.`,
el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
首先通过 findPrevElement
函数找到当前元素的前一个元素描述对象,并将其赋值给 prev
常量,接着进入 if
条件语句,判断当前元素的前一个元素是否使用了 v-if
指令,我们知道对于使用了 v-else-if
或 v-else
指令的元素来讲,他们的前一个元素必然需要使用相符的 v-if
指令才行。如果前一个元素确实使用了 v-if
指令,那么则会调用 addIfCondition
函数将当前元素描述对象添加到前一个元素的 ifConditions
数组中。如果前一个元素没有使用 v-if
指令,那么此时将会进入 else...if
条件语句的判断,即如果是非生产环境下,会打印警告信息提示开发者没有相符的使用了 v-if
指令的元素。
以上是当前元素使用了 v-else-if
或 v-else
指令时的特殊处理,由此可知 当一个元素使用了 v-else-if
或 v-else
指令时,它们是不会作为父级元素子节点的,而是会被添加到相应的使用了 v-if
指令的元素描述对象的 ifConditions
数组中。
# 2.9 findPrevElement
findPrevElement
函数的作用是寻找当前元素的前一个元素节点,如下是其源码:
function findPrevElement (children: Array<any>): ASTElement | void {
let i = children.length
while (i--) {
if (children[i].type === 1) {
return children[i]
} else {
if (process.env.NODE_ENV !== 'production' && children[i].text !== ' ') {
warn(
`text "${children[i].text.trim()}" between v-if and v-else(-if) ` +
`will be ignored.`,
children[i]
)
}
children.pop()
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
首先 findPrevElement
函数只用在了 processIfConditions
函数中,它的作用就是当解析器遇到一个带有 v-else-if
或 v-else
指令的元素时,找到该元素的前一个元素节点。
我们还是以前面分析 processIf
时的案例来说明,如下:
<div>
<div v-if="child === 1"> if </div>
<div v-else-if="child === 2"> else if </div>
<div v-else> else </div>
</div>
2
3
4
5
当解析器遇到带有 v-else-if
指令的 div
标签时,那么此时它的前一个元素节点应该是带有 v-if
指令的 div
标签,如何找到该 div
标签呢?由于当前正在解析的标签为 div
,此时 div
标签的元素描述对象还没有被添加到父级元素描述对象的 children
数组中,所以此时父级元素描述对象的 children
数组中最后一个元素节点就应该是带有 v-if
指令 div
元素。注意我们说的是 最后一个元素节点,而不是 最后一个节点。所以要想得到带有 v-if
指令 div
标签,我们只要找到父级元素描述对象的 children
数组最后一个元素节点即可。
当解析器遇到带有 v-else
指令的 div
标签时,大家思考一下此时 div
标签的前一个 元素节点 是什么?答案还是带有 v-if
指令 div
标签,而不是带有 v-else-if
指令 div
标签,这是因为带有 v-else-if
指令 div
标签的元素描述对象没有被添加到父级元素描述对象的 children
数组中,而是被添加到带有 v-if
指令 div
标签元素描述对象的 ifConditions
数组中了。所以对于带有 v-else
指令 div
标签来讲,它的前一个元素节点仍然是带有 v-if
指令 div
标签。
总之我们发现 findPrevElement
函数只需要找到父级元素描述对象的最后一个元素节点即可。
# 2.10 processKey
在分析 closeElement
是我们预留了一个 processElement
没有分析,由于 processElement
函数设计到的逻辑函数比较多,我们先从内部其他的函数开始分析,分析完其他函数我们再回过头来分析 processElement
,首先我们看一下 processKey
的定义如下:
源码目录:src/compiler/parser/index.js
export function processElement (
element: ASTElement,
options: CompilerOptions
) {
processKey(element)
/* 省略 */
}
function processKey (el) {
const exp = getBindingAttr(el, 'key')
if (exp) {
if (process.env.NODE_ENV !== 'production') {
if (el.tag === 'template') {
warn(
`<template> cannot be keyed. Place the key on real elements instead.`,
getRawBindingAttr(el, 'key')
)
}
if (el.for) {
const iterator = el.iterator2 || el.iterator1
const parent = el.parent
if (iterator && iterator === exp && parent && parent.tag === 'transition-group') {
warn(
`Do not use v-for index as key on <transition-group> children, ` +
`this is the same as not using keys.`,
getRawBindingAttr(el, 'key'),
true /* tip */
)
}
}
}
el.key = exp
}
}
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
说明:关于 processElement
我们会在后面的小节中做详细分析。
在分析 processKey
之前我们还是以一个案例进行分析,如下:
<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
processKey
首先调用 getBindingAttr
对 AST
树进行处理,getBindingAttr
的作用是从元素描述对象的 attrsList
数组中获取到属性名字为 key
的属性值,并将值赋值给 exp
常量。
我们再来一下 getBindingAttr
的定义,如下:
源码目录:src/compiler/helpers.js
export function getBindingAttr (
el: ASTElement,
name: string,
getStatic?: boolean
): ?string {
const dynamicValue =
getAndRemoveAttr(el, ':' + name) ||
getAndRemoveAttr(el, 'v-bind:' + name)
if (dynamicValue != null) {
return parseFilters(dynamicValue)
} else if (getStatic !== false) {
const staticValue = getAndRemoveAttr(el, name)
if (staticValue != null) {
return JSON.stringify(staticValue)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
getBindingAttr
首先通过执行 getAndRemoveAttr
从元素描述对象的 attrsList
数组中获取到属性名字为参数name
的值所对应的属性值,并赋值给变量 dynamicValue
。
接下来通过判断绑定属性本身是否存在,当不存在时获取到的属性值为 undefined
,所以执行 elseif
语句,当存在是获取到的属性值不为 undefined
,则执行 if
语句。
我们首先来看一下 elseif
语句,当不传递第三个参数时 elseif
分支的条件默认成立。elseif
语句块内代码的作用是用来获取非绑定的属性值,因为代码既然执行到了 elseif
分支,则说明此时获取绑定的属性值失败,我们知道当我们为元素或组件添加属性时,这个属性可以是绑定的也可以是非绑定的,所以当获取绑定的属性失败时,我们不能够武断的认为开发者没有编写该属性,而是应该继续尝试获取非绑定的属性值。
非绑定属性值的获取方式同样是使用 getAndRemoveAttr
函数,只不过此时传递给该函数的第二个参数是原始的属性名字,不带有 v-bind:
或 :
。同时将获取结果保存在 staticValue
常量中,接着进入一个条件判断,如果属性值存在则使用 JSON.stringify
函数对属性值进行处理后将其返回。
大家注意 JSON.stringify
函数对属性值的处理至关重要,这么做能够保证对于非绑定的属性来讲,总是会将该属性的值作为字符串处理。
我们再来看一下 if
语句,它是通过调用 parseFilters
以 dynamicValue
为参数,那么 parseFilters
具体是做什么呢?我们先来看一下它的定义,如下:
源码目录:src/compiler/parser/filter-parser.js
/* @flow */
// 匹配 ) 或 . 或 + 或 - 或 _ 或 $ 或 ]
const validDivisionCharRE = /[\w).+\-_$\]]/
/**
* 解析成正确的value,
* 把过滤器转换成vue虚拟dom的解析方法函数
* 比如把过滤器 'message | filterA | filterB('arg1', arg2)' 转换成 _f("filterB")(_f("filterA")(message),arg1,arg2)
* @param {表达式} exp
*/
export function parseFilters (exp: string): string {
// 是否在 ''中
let inSingle = false
// 是否在 "" 中
let inDouble = false
// 是否在 ``
let inTemplateString = false
// 是否在 正则 \\ 中
let inRegex = false
// 是否在 `{` 中发现一个 culy加1,然后发现一个 `}` culy减1,直到culy为0,说明 { .. }闭合
let curly = 0
// 跟 `{` 一样有一个 `[` 加1, 有一个 `]` 减1
let square = 0
// 跟 `{` 一样有一个 `(` 加1, 有一个 `)` 减1
let paren = 0
// 属性值字符串中字符的索引,将会被用来确定过滤器的位置
let lastFilterIndex = 0
// c: 当前读入字符所对应的 ASCII 码
// prev: 当前字符的前一个字符所对应的 ASCII 码
// i: 当前读入字符的位置索引
// expression: 是 parseFilters 函数的返回值
// filters: 是一个数组,它保存着所有过滤器函数名
let c, prev, i, expression, filters
// 将属性值字符串作为字符流读入,从第一个字符开始一直读到字符串的末尾
for (i = 0; i < exp.length; i++) {
prev = c // 将上一次读取的字符所对应的 ASCII 码赋值给 prev 变量
c = exp.charCodeAt(i) // 设置为当前读取字符所对应的 ASCII 码
if (inSingle) { // 如果当前读取的字符存在于由单引号包裹的字符串内,则会执行这里的代码
// c === `'` && pre !== `\`,当前字符是单引号('),并且当前字符的前一个字符不是反斜杠(\)
if (c === 0x27 && prev !== 0x5C) inSingle = false
} else if (inDouble) { // 如果当前读取的字符存在于由双引号包裹的字符串内,则会执行这里的代码
// c === `"` && pre !== `\`,当前字符是双引号('),并且当前字符的前一个字符不是反斜杠(\)
if (c === 0x22 && prev !== 0x5C) inDouble = false
} else if (inTemplateString) { // 如果当前读取的字符存在于模板字符串内,则会执行这里的代码
// c === ``` && pre !== `\`,当前字符是(`),并且当前字符的前一个字符不是反斜杠(\)
if (c === 0x60 && prev !== 0x5C) inTemplateString = false
} else if (inRegex) { // 如果当前读取的字符存在于正则表达式内,则会执行这里的代码
// c === `/` && pre !== `\`,当前字符是斜杠(/),并且当前字符的前一个字符不是反斜杠(\)
if (c === 0x2f && prev !== 0x5C) inRegex = false
} else if (
// 1. 0x7C ===> `|`,当前字符必须是管道
// 2. 该字符的后一个字符不能是管道符
// 3. 该字符的前一个字符不能是管道符
// 4. 该字符不能处于 {} 中
// 5. 该字符不能处于 [] 中
// 6. 该字符不能处于 () 中
// 字符满足以上条件,则说明该字符就是用来作为过滤器分界线的管道符
c === 0x7C && // pipe
exp.charCodeAt(i + 1) !== 0x7C &&
exp.charCodeAt(i - 1) !== 0x7C &&
!curly && !square && !paren
) { // 如果当前读取的字符是过滤器的分界线,则会执行这里的代码
if (expression === undefined) {
// first filter, end of expression
// 过滤器表达式,就是管道符号之后开始
lastFilterIndex = i + 1
// 存储过滤器的表达式
// 例如:这里匹配如果字符串是 'ab|c' 则把ab匹配出来
expression = exp.slice(0, i).trim()
} else { // 当不满足以上条件时,执行这里的代码
pushFilter()
}
} else {
switch (c) {
// 如果当前字符为双引号("),则将 inDouble 变量的值设置为 true
// 如果当前字符为单引号('),则将 inSingle 变量的值设置为 true
// 如果当前字符为模板字符串的定义字符(`),则将 inTemplateString 变量的值设置为 true
// 如果当前字符是左圆括号((),则将 paren 变量的值加一
// 如果当前字符是右圆括号()),则将 paren 变量的值减一
// 如果当前字符是左方括号([),则将 square 变量的值加一
// 如果当前字符是右方括号(]),则将 square 变量的值减一
// 如果当前字符是左花括号({),则将 curly 变量的值加一
// 如果当前字符是右花括号(}),则将 curly 变量的值减一
case 0x22: inDouble = true; break // "
case 0x27: inSingle = true; break // '
case 0x60: inTemplateString = true; break // `
case 0x28: paren++; break // (
case 0x29: paren--; break // )
case 0x5B: square++; break // [
case 0x5D: square--; break // ]
case 0x7B: curly++; break // {
case 0x7D: curly--; break // }
}
if (c === 0x2f) { // /
let j = i - 1 // 变量 j 是 / 字符的前一个字符的索引
let p
// find first non-whitespace prev char
// 是找到 / 字符之前第一个不为空的字符
for (; j >= 0; j--) {
p = exp.charAt(j)
if (p !== ' ') break
}
// 如果字符 / 之前没有非空的字符,或该字符不满足正则 validDivisionCharRE 的情况下,才会认为字符 / 为正则的开始
if (!p || !validDivisionCharRE.test(p)) {
inRegex = true
}
}
}
}
if (expression === undefined) {
expression = exp.slice(0, i).trim()
} else if (lastFilterIndex !== 0) {
pushFilter()
}
/**
* 获取当前过滤器的,并将其存储在filters 数组中
* filters = [ 'filterA' , 'filterB']
*/
function pushFilter () {
// 检查变量 filters 是否存在,如果不存在则将其初始化为空数组
// 接着使用 slice 方法对字符串 exp 进行截取,截取的开始和结束位置恰好是 lastFilterIndex(指的是管道符后面的第一个字符) 和 i
(filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
lastFilterIndex = i + 1
}
if (filters) {
for (i = 0; i < filters.length; i++) {
// 把过滤器封装成函数,虚拟dom需要渲染的函数
expression = wrapFilter(expression, filters[i])
}
}
return expression
}
/**
* 生成过滤器的表达式字符串
* 例如:
* {{ message | filterA | filterB('arg1', arg2) }}
* 第一步 以exp 为入参 生成 filterA 的过滤器表达式字符串
* _f("filterA")(message)
* 第二步 以第一步字符串作为入参,生成第二个过滤器的表达式字符串
* _f("filterB")(_f("filterA")(message),arg1,arg2)
* @param {上一个过滤器的值,没有就是表达式的值} exp
* @param {过滤器} filter
*/
function wrapFilter (exp: string, filter: string): string {
// 返回字符串第一次出现'('索引的位置
const i = filter.indexOf('(')
if (i < 0) {
// _f: resolveFilter
return `_f("${filter}")(${exp})`
} else {
// name 是从字符串开始到 `(` 结束的字符串,不包含 `(`
const name = filter.slice(0, i)
// args是从 `(` 开始匹配,到字符串末端,不包含`(`
const args = filter.slice(i + 1)
return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`
}
}
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
关于 parseFilters
函数的分析上面的源码中的注视已经详细说明了,我们就不再重复分析,接着我们再回到 processKey
函数,如果一个标签使用了 key
属性,在开发环境判断标签是否为 template
,如果 el.tag
为 template
则报警告,或者el.tag
为 transition-group
并且 iterator === exp
的的时候,也会报警告,最后将该标签的元素描述对象上将被添加 el.key
属性。
# 2.11 processRef
老规矩在分析 processRef
之前我们函数找个案例来进行说明,如下:
<ul :class="classObject" class="list" v-show="isShow">
<li v-for="(l, i) in list" :key="i" ref="i" @click="clickItem(index)">{{ i }}:{{ l }}</li>
</ul>
2
3
这个案例我们是在前面案例的基础上给 li
添加了 ref
属性,接下来我们看一下 processRef
的定义,如下:
源码目录:src/compiler/parser/index.js
export function processElement (
element: ASTElement,
options: CompilerOptions
) {
/* 省略 */
processRef(element)
/* 省略 */
}
function processRef (el) {
const ref = getBindingAttr(el, 'ref')
if (ref) {
el.ref = ref
el.refInFor = checkInFor(el)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
processRef
一开始就调用 getBindingAttr
函数获取元素 ref
属性的值,并将结果赋值给 ref
常量。
在们这个案例中获取到的 ref
为 "i"
,即 ref = ""i""
,因为 ref
不为空所以继续执行 if
语句,首先在元素描述对象上添加 el.ref
属性,接着在元素描述对象上添加 el.refInFor
属性,该属性是一个布尔值,标识着这个使用了 ref
属性的标签是否存在于 v-for
指令之内
说明:关于 getBindingAttr
我们在上一小节已经做了详细分析,同学们如果忘了的话可以查看上一小节的内容。
接下我们看一下 checkInFor
的定义,源码如下:
源码目录:src/compiler/parser/index.js
function checkInFor (el: ASTElement): boolean {
let parent = el
while (parent) {
if (parent.for !== undefined) {
return true
}
parent = parent.parent
}
return false
}
2
3
4
5
6
7
8
9
10
首先将当前元素 el
保存到 parent
,然后通过 while
循环从当前元素开始,逐层向父级节点遍历,直到根节点为止,如果发现某标签的元素描述对象的 for
属性不为 undefined
,则函数返回 true
,意味着当前元素所使用的 ref
属性存在于 v-for
指令之内。否则 checkInFor
函数会返回 false
,代表当前元素所使用的 ref
属性不在 v-for
指令之内。
# 2.12 processSlotContent
在分析插槽之前,我们总结一下与插槽相关的使用形式:
V2.6
以前
- 1、默认插槽:
// 子组件template
<div>
Hello,World!
<slot></slot>
</div>
// 调用子组件
<child-component>你好</child-component>
2
3
4
5
6
7
8
- 2、具名插槽
// 子组件template
<div>
<h4>这个世界不仅有男人和女人</h4>
<slot name="girl"></slot>
<div style="height:1px;background-color:red;"></div>
<slot name="boy"></slot>
<div style="height:1px;background-color:red;"></div>
<slot></slot>
</div>
// 调用子组件
<child-component>
<template slot="girl">
漂亮、美丽、购物、逛街
</template>
<template slot="boy">
帅气、才实
</template>
<div>
我是一类人,
我是默认的插槽
</div>
</child-component>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 3、作用域插槽 - slot-scope
// 子组件template
<div>
<slot say="你好"></slot>
</div>
// 调用子组件
<child-component>
<template slot-scope="say">
<!-- {"say":"你好"} -->
{{ say }}
</template>
</child-component>
2
3
4
5
6
7
8
9
10
11
12
V2.6
以后
- 1、默认插槽:
// 子组件template
<div>
Hello,World!
<slot></slot>
</div>
// 调用子组件
<child-component>你好</child-component>
2
3
4
5
6
7
8
- 2、具名插槽
// 子组件template
<div>
<h4>这个世界不仅有男人和女人</h4>
<slot name="girl"></slot>
<div style="height:1px;background-color:red;"></div>
<slot name="boy"></slot>
<div style="height:1px;background-color:red;"></div>
<slot></slot>
</div>
// 调用子组件
<child-component>
<template v-slot:girl>
漂亮、美丽、购物、逛街
</template>
<template v-slot:boy>
帅气、才实
</template>
<template v-slot:default>
我是一类人,
我是默认的插槽
</template>
</child-component>
// 调用子组件——简写形式
<child-component>
<template #girl>
漂亮、美丽、购物、逛街
</template>
<template #boy>
帅气、才实
</template>
<template #default>
我是一类人,
我是默认的插槽
</template>
</child-component>
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
- 3、作用域插槽 - slot-scope
// 子组件template
<!-- todos = [{ name: 'list1', id: 1 }, { name: 'list2', id: 2 }, { name: 'list3', id: 3 }] -->
<ul>
<li v-for="(todo, i) in todos" :key="i">
<slot name="todo" :todo="todo"></slot>
</li>
</ul>
// 调用子组件
<child-component>
<!-- v-slot:todo 代表具名插槽 todo,所以子组件必须为具名插槽 -->
<template v-slot:todo="todo">
<!-- {"todo":{ name: 'list1', id: 1 }} -->
{{ todo }}
</template>
</child-component>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
注意: v-slot
只能添加在 <template>
上 (只有一种例外情况 (opens new window)),这一点和已经废弃的 slot
attribute (opens new window) 不同。
说明:关于 v-slot
跟多内容可以移步到 这里 (opens new window) 学习。
总结完插槽在不同版本下的形式, processSlotContent
函数就是用来解析以上标签并为这些标签的描述对象添加相应属性,由于 processSlotContent
的内容比较多,我们分开一小部分一小部分的分析,如下:
源码目录:src/compiler/parser/index.js
function processSlotContent (el) {
let slotScope
if (el.tag === 'template') {
slotScope = getAndRemoveAttr(el, 'scope')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && slotScope) {
warn(
`the "scope" attribute for scoped slots have been deprecated and ` +
`replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
`can also be used on plain elements in addition to <template> to ` +
`denote scoped slots.`,
el.rawAttrsMap['scope'],
true
)
}
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
warn(
`Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
`(v-for takes higher priority). Use a wrapper <template> for the ` +
`scoped slot to make it clearer.`,
el.rawAttrsMap['slot-scope'],
true
)
}
el.slotScope = slotScope
}
/* 省略 */
}
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
首先 if
语句判断 el.tag === 'template'
标签是 template
,通过 getAndRemoveAttr
移除 element.attrsList
对象中 name
为 scope
的属性,并且返回获取到 scope
属性的值赋值给变量 slotScope
,然后在非生产环境下,如果 slotScope
变量存在,则说明 <template>
标签中使用了 scope
属性,但是这个属性已经在 2.5.0+
的版本中被 slot-scope
属性替代了,所以现在更推荐使用 slot-scope
属性,好处是 slot-scope
属性不受限于 <template>
标签。最后在元素描述对象上添加了 el.slotScope
属性,如果 slotScope
变量的值存在,则使用 slotScope
变量的值,否则通过 getAndRemoveAttr
函数获取当前标签 slot-scope
属性的值作为 el.slotScope
属性的值。
紧接着我们再来看 else if
语句,else if
语句类似 if
语句的逻辑 ,如果标签不是 template
,通过 getAndRemoveAttr
移除 element.attrsList
对象中 name
为 slot-scope
的属性,并且返回获取到 slot-scope
属性的值赋值给变量 slotScope
,最后在元素描述对象上添加了 el.slotScope
属性。
else if
语句中,还一段逻辑作用是,在非生产环境下,会检查当前元素是否使用了 v-for
属性,如下代码所示:
<div slot-scope="slotProps" v-for="item of slotProps.list"></div>
如上这句代码中,slot-scope
属性与 v-for
指令共存,这会造成什么影响呢?由于 v-for
具有更高的优先级,所以 v-for
绑定的状态将会是父组件作用域的状态,而不是子组件通过作用域插槽传递的状态。并且这么使用很容易让人感到困惑。更好的方式是像如下代码这样:
<template slot-scope="slotProps">
<div v-for="item of slotProps.list"></div>
</template>
2
3
这样就不会有任何歧义,v-for
指令绑定的状态就是作用域插槽传递的状态。而上面代码的警告信息,大概就是这个意思。
我们继续往下看,源码如下:
源码目录:src/compiler/parser/index.js
function processSlotContent (el) {
/* 省略 */
// slot="xxx"
const slotTarget = getBindingAttr(el, 'slot')
if (slotTarget) {
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot'])
// preserve slot as an attribute for native shadow DOM compat
// only for non-scoped slots.
if (el.tag !== 'template' && !el.slotScope) {
addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
}
}
/* 省略 */
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这段代码主要用来处理标签的 slot
属性,首先使用 getBindingAttr
函数获取元素 slot
属性的值,并将获取到的值赋值给 slotTarget
常量,注意这里使用的是 getBindingAttr
函数,这意味着 slot
属性是可以绑定的。
紧接着检测了 slotTarget
变量是否为字符串 '""'
,这种情况出现在标签虽然使用了 slot
属性,但却没有为 slot
属性指定相应的值,如下:
<div slot></div>
这时通过 getBindingAttr
函数获取 slot
属性的值时,会得到字符串 ""
,此时会将 el.slotTarget
属性的值设置为字符串 '"default"'
,否则直接将 slotTarget
变量的值赋值给 el.slotTarget
属性。
然后通过 el.attrsMap[':slot'] || el.attrsMap['v-bind:slot']
获取动态绑定的 slot
的属性并通过 !!
转换为布尔类型,并在元素描述对象上添加了 el.slotTargetDynamic
属性,值为转换后的布尔值。
接下来是一个 if
语句,这段代码的作用就是用来保存原生影子DOM
(shadow DOM
)的 slot
属性,当然啦既然是原生影子 DOM
的 slot
属性,那么首先该元素必然应该是原生 DOM
,所以 el.tag !== 'template'
必须成立,同时对于作用域插槽是不会保留原生 slot
属性的。关于原生影子 DOM
的 slot
属性,更详细的内容大家可以阅读 Element.slot (opens new window)。你会发现 Vue
的实现是在一定程度上参考了标准的。保留原生 slot
属性的方式,就是调用 addAttr
函数,我们知道该函数会将属性的名字和值以对象的形式添加到元素描述对象的 el.attrs
数组中。
我们继续往下分析,源码如下:
源码目录:src/compiler/parser/index.js
function processSlotContent (el) {
/* 省略 */
// 2.6 v-slot syntax
if (process.env.NEW_SLOT_SYNTAX) {
if (el.tag === 'template') {
// v-slot on <template>
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
if (process.env.NODE_ENV !== 'production') {
if (el.slotTarget || el.slotScope) {
warn(
`Unexpected mixed usage of different slot syntaxes.`,
el
)
}
if (el.parent && !maybeComponent(el.parent)) {
warn(
`<template v-slot> can only appear at the root level inside ` +
`the receiving component`,
el
)
}
}
const { name, dynamic } = getSlotName(slotBinding)
el.slotTarget = name
el.slotTargetDynamic = dynamic
el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
}
} else {
// v-slot on component, denotes default slot
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
if (process.env.NODE_ENV !== 'production') {
if (!maybeComponent(el)) {
warn(
`v-slot can only be used on components or <template>.`,
slotBinding
)
}
if (el.slotScope || el.slotTarget) {
warn(
`Unexpected mixed usage of different slot syntaxes.`,
el
)
}
if (el.scopedSlots) {
warn(
`To avoid scope ambiguity, the default slot should also use ` +
`<template> syntax when there are other named slots.`,
slotBinding
)
}
}
// add the component's children to its default slot
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.slotScope) {
c.parent = slotContainer
return true
}
})
slotContainer.slotScope = slotBinding.value || emptySlotScopeToken
// remove children as they are returned from scopedSlots now
el.children = []
// mark el non-plain so data gets generated
el.plain = false
}
}
}
}
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
这段代码是在 V2.6+
中新增 v-slot
的处理逻辑,主要分为两种情况分别为template
标签和组件。
template
这种情况下执行 if
语句里面的代码,首先通过 getAndRemoveAttrByRegex
获取 v-slot
或者 v-slot:xxx
或 #xxx
的值,例如 v-slot:todo="todo"
,对应的 attrsList
为 attrsList: [{name: "v-slot:todo", value: "todo", start: 76, end: 87}]
,所以 匹配获取到 slotBinding
为{name: "v-slot:todo", value: "todo", start: 76, end: 87}
,源码如下:
源码目录:src/compiler/helper.js
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]
// 例如:匹配到 'v-slot' 或者 'v-slot:xxx' 或 '#xxx' 则会返回其对应的 attr
if (name.test(attr.name)) {
list.splice(i, 1) // // 删除数组中对应数据项
return attr
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
我们回到 processSlotContent
继续往下看,接下来判断 el.slotScope
或 el.slotTarget
存在,说明使用了 slot
或 slot-scope
,此时在开发环境报警告:新旧语法不能混合使用。接下来还是一个 if
判断作用是取标签的父级标签,如果父级标签不是组件或者 template
标签,此时在开发环境报警告:只能出现在组件的根级别,例如下列这种情况就是不被允许的,代码如下:
// 子组件template
<ul>
<li v-for="(todo, i) in todos" :key="i">
<slot name="todo" :todo="todo"></slot>
</li>
</ul>
// 调用子组件
<child>
<div>
<template #todo="todo">
{{ todo }}
</template>
</div>
</child>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我们继续往下看剩余的代码,如下:
const { name, dynamic } = getSlotName(slotBinding)
el.slotTarget = name
el.slotTargetDynamic = dynamic
el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
2
3
4
例如我们前面的案例获取到的slotBinding
为{name: "v-slot:todo", value: "todo", start: 76, end: 87}
,则通过 getSlotName
获取到的 name, dynamic
分别为 { name: '"todo"', dynamic: false }
,如果slotBinding
为{name: "v-slot:[todo]", value: "todo", start: 76, end: 87}
,则getSlotName
获取到的 name, dynamic
分别为 { name: 'todo', dynamic: true }
。最后在元素描述对象上分别添加 el.slotTarget
、el.slotTargetDynamic
、el.slotScope
属性。
- 组件
说明:关于插槽的这种用法,可以查看 独占默认插槽的缩写语法 (opens new window)。
我们继续分析 else
语句 ,首先也是通过 getAndRemoveAttrByRegex
获取 v-slot
或者 v-slot:xxx
或 #xxx
的值,我们还是以前面的案例来分析,例如 :
<current-user v-slot:default="todo">
{{ todo.user.firstName }}
</current-user>
<!--或-->
<current-user v-slot="todo">
{{ todo.user.firstName }}
</current-user>
2
3
4
5
6
7
v-slot:default="todo"
,对应的 attrsList
为 attrsList: [{name: "v-slot:default", value: "todo", start: 76, end: 87}]
,所以 匹配获取到 slotBinding
为{name: "v-slot:todo", value: "todo", start: 76, end: 87}
。
接下来也是在开发环境判断当前标签不是组件或template
标签,则报警告:v-slot
只能在组件和template
中使用,接下来还是一个判断即 el.slotScope
或 el.slotTarget
存在,说明使用了 slot
或 slot-scope
,此时在开发环境报警告:新旧语法不能混合使用,接下来又是一个 if
判断 el.scopedSlots
存在 ,说明组件内部不止一个默认插槽,此时还有其他插槽,则报警告:为了避免范围模糊,默认的 slot
还应该使用<template>
语法,即默认插槽的缩写语法不能和具名插槽混用。
我们继续往下看剩余的代码,如下:
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.slotScope) {
c.parent = slotContainer
return true
}
})
slotContainer.slotScope = slotBinding.value || emptySlotScopeToken
// remove children as they are returned from scopedSlots now
el.children = []
// mark el non-plain so data gets generated
el.plain = false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这段代码的主要作用是:
- 获取当前组件的
scopedSlots
- 获取到
name, dynamic
- 获取
slots
中key
对应匹配出来name
的slot
,然后再其下面创建一个标签名为template
的ASTElement
,attrs
为空数组,parent
为当前节点 name、dynamic
统一赋值给slotContainer
的slotTarget、slotTargetDynamic
,而不是el
- 将当前节点的
children
添加到slotContainer
的children
属性中 - 清空当前节点的
children
el.plain
设置为false
在上面分析过程中我们我们还剩余了 getSlotName
,定义如下:
function getSlotName (binding) {
let name = binding.name.replace(slotRE, '')
if (!name) {
if (binding.name[0] !== '#') {
name = 'default' // 赋值默认名
} else if (process.env.NODE_ENV !== 'production') {
warn(
`v-slot shorthand syntax requires a slot name.`,
binding
)
}
}
return dynamicArgRE.test(name)
// dynamic [name]
? { name: name.slice(1, -1), dynamic: true }
// static name
: { name: `"${name}"`, dynamic: false }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
我们还是以上面的案例来分析,例如我们上面获取到的 slotBinding
为{name: "v-slot:todo", value: "todo", start: 76, end: 87}
,getSlotName
首先通过正则表达式 slotRE
将 v-slot
或 v-slot:
或 #
替换为空字符,然后判断 name
是否存在,当 name
不存在时,并且 name
的第一个字符不是 #
时,name
赋值为 default
;否则在开发环境报警告:简写模式下必须有 name
。
最后通过判断 name
是否为动态名称返回不同的对象,我们在前面几章的学习中知道 dynamicArgRE
用来匹配以字符 [
开头并以字符 ]
结尾的字符串,作用是判断是否为动态属性。所以如果 name
为动态名称 即[todo]
,则getSlotName
获取到的 name, dynamic
分别为 { name: 'todo', dynamic: true }
,如果 name
为静态名称 即todo
,则通过 getSlotName
获取到的 name, dynamic
分别为 { name: '"todo"', dynamic: false }
。
# 2.13 processSlotOutlet
我们继续往下看,接下来是调用 processSlotOutlet
,定义如下:
源码目录:src/compiler/parser/index.js
function processSlotOutlet (el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name')
if (process.env.NODE_ENV !== 'production' && el.key) {
warn(
`\`key\` does not work on <slot> because slots are abstract outlets ` +
`and can possibly expand into multiple elements. ` +
`Use the key on a wrapping element instead.`,
getRawBindingAttr(el, 'key')
)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
通过 if
语句的条件:el.tag === 'slot'
,可知 if
语句块内的代码是用来处理 插槽标签的,所以如果当前标签是
标签,则 if
语句块内的代码将会被执行,在 if
语句块内,首先通过 getBindingAttr
函数获取标签的 name
属性值,并将获取到的值赋值给元素描述对象的 el.slotName
属性。举个例子,如果我们的 <slot>
标签如下:
<slot name="header"></slot>
则 el.slotName
属性的值为 JSON.stringify('header')
。
如果我们的 <slot>
标签如下:
<slot></slot>
则 el.slotName
属性的值为 undefined
。
获取插槽的名字之后,会执行如下代码:
if (process.env.NODE_ENV !== 'production' && el.key) {
warn(
`\`key\` does not work on <slot> because slots are abstract outlets ` +
`and can possibly expand into multiple elements. ` +
`Use the key on a wrapping element instead.`
)
}
2
3
4
5
6
7
在非生产环境下,如果发现在 <slot>
标签中使用 key
属性,则会打印警告信息,提示开发者 key
属性不能使用在 slot
标签上,另外大家应该还记得,在前面的分析中我们也知道 key
属性同样不能使用在 <template>
标签上。大家可以发现 <slot>
标签和 <template>
标签的共同点就是他们都是抽象组件,抽象组件的特点是要么不渲染真实 DOM
,要么会被不可预知的 DOM
元素替代。
# 2.14 processComponent
我们继续往下看,接下来是调用 processComponent
,定义如下:
源码目录:src/compiler/parser/index.js
export function processElement (
element: ASTElement,
options: CompilerOptions
) {
/* 省略 */
processComponent(element)
/* 省略 */
}
function processComponent (el) {
let binding
if ((binding = getBindingAttr(el, 'is'))) {
el.component = binding
}
if (getAndRemoveAttr(el, 'inline-template') != null) {
el.inlineTemplate = true
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
第一,定义一个 binding
变量,然后通过调用 getBindingAttr
函数获取元素的 is
属性对应的值,如果获取成功,则会将取到的值赋值给元素描述对象的 el.component
属性。
第二,通过调用 getAndRemoveAttr
函数获取元素的 inline-template
属性对应的值,如果获取成功,则将元素描述对象的 el.inlineTemplate
属性设置为 true
,代表着该标签使用了 inline-template
属性。
# 2.15 processAttrs
最后我们接下来我们要讲解的就是 processAttrs
函数,这个函数是用来处理元素描述对象的 el.attrsList
数组中剩余的所有属性的。
通过前面的分析我们能够发现一些规律,比如在获取这些属性的值的时候,要么使用 getAndRemoveAttr
函数,要么就使用 getBindingAttr
函数,但是无论使用哪个函数,其共同的行为是:在获取到特定属性值的同时,还会将该属性从 el.attrsList
数组中移除。所以在调用 processAttrs
函数的时候,以上列出来的属性都已经从 el.attrsList
数组中移除了。但是 el.attrsList
数组中仍然可能存在其他属性,所以这个时候就需要使用 processAttrs
函数处理 el.attrsList
数组中剩余的属性。
我们发现凡是以 v-
开头的属性,在获取属性值的时候都是通过 getAndRemoveAttr
函数获取的。而对于没有 v-
开头的特性,如 key
、ref
等,在获取这些属性的值时,是通过 getBindingAttr
函数获取的,不过 slot-scope
、scope
和 inline-template
这三个属性虽然没有以 v-
开头,但仍然使用 getAndRemoveAttr
函数获取其属性值。但这并不是关键,关键的是我们要知道使用 getAndRemoveAttr
和 getBindingAttr
这两个函数获取属性值的时候到底有什么区别。
我们知道类似于 v-for
或 v-if
这类以 v-
开头的属性,在 Vue
中我们称之为指令,并且这些属性的属性值是默认情况下被当做表达式处理的,比如:
<div v-if="a && b"></div>
如上代码在执行的时候 a
和 b
都会被当做变量,并且 a && b
是具有完整意义的表达式,而非普通字符串。并且在解析阶段,如上 div
标签的元素描述对象的 el.attrsList
属性将是如下数组:
el.attrsList = [
{
name: 'v-if',
value: 'a && b'
}
]
2
3
4
5
6
这时,当使用 getAndRemoveAttr
函数获取 v-if
属性值时,得到的就是字符串 'a && b'
,但不要忘了这个字符串最终是要运行在 new Function()
函数中的,假设是如下代码:
new Function('a && b')
那么这句代码等价于:
function () {
a && b
}
2
3
可以看到,此时的 a && b
已经不再是普通字符串了,而是表达式。
这就意味着 slot-scope
、scope
和 inline-template
这三个属性的值,最终也将会被作为表达式处理,而非普通字符串。如下:
<div slot-scope="slotProps"></div>
如上代码是使用作用域插槽的典型例子,我们知道这里的 slotProps
确实是变量,而非字符串。
那如果使用 getBindingAttr
函数获取 slot-scope
属性的值会产生什么效果呢?由于 slot-scope
并非 v-bind:slot-scope
或 :slot-scope
,所以在使用 getBindingAttr
函数获取 slot-scope
属性值的时候,将会得到使用 JSON.stringify
函数处理后的结果,即:
JSON.stringify('slotProps')
这个值就是字符串 '"slotProps"'
,我们把这个字符串拿到 new Function()
中,如下:
new Function('"slotProps"')
如上这句代码等价于:
function () {
"slotProps"
}
2
3
可 以发现此时函数体内只有一个字符串 "slotProps"
,而非变量。
但并不是说使用了 getBindingAttr
函数获取的属性值最终都是字符串,如果该属性是绑定的属性(使用 v-bind
或 :
),则该属性的值仍然具有 javascript
语言的能力。否则该属性的值就是一个普通的字符串。
如下是前面已经处理过的属性:
v-pre
v-for
v-if
、v-else-if
、v-else
v-once
key
ref
slot
、slot-scope
、scope
、name
、v-slot
is
、inline-template
如上属性中包含了部分 Vue
内置的指令(v-
开头的属性),大家可以对照一下 Vue
的官方文档,查看其内置的指令,可以发现之前的讲解中不包含对以下指令的解析:
v-text
、v-html
、v-show
、v-on
、v-bind
、v-model
、v-cloak
除了这些指令之外,还有部分属性的处理我们也没讲到,比如 class
属性和 style
属性,这两个属性比较特殊,因为 Vue
对他们做了增强,实际上在transforms
中有对于 class
属性和 style
属性的处理,这个我们后面会统一讲解。
以如上列出的属性为例,下表中总结了特定的属性与获取该属性值的方式:
属性 | 获取属性值的方式 |
---|---|
v-pre | getAndRemoveAttr |
v-for | getAndRemoveAttr |
v-if 、v-else-if 、v-else | getAndRemoveAttr |
v-once | getAndRemoveAttr |
key | getBindingAttr |
ref | getBindingAttr |
name | getBindingAttr |
slot-scope 、scope | getAndRemoveAttr |
slot | getBindingAttr |
v-slot | getAndRemoveAttrByRegex |
is | getBindingAttr |
inline-template | getAndRemoveAttr |
老规矩在分析之前我们还是以一个案例来分析,源码如下:
<div :class="classObject" class="list" v-show="isShow" @click="clickItem(index)">
{{ val }}
</div>
2
3
我们这个案例经过 createASTElement
生成的 AST
树为,如下:
{
"type":1,
"tag":"div",
"attrsList":[
{"name":":class","value":"classObject","start":5,"end":25},
{"name":"class","value":"list","start":26,"end":38},
{"name": "v-bind:prop1", "value": "prop1", "start": 39, "end": 59},
{"name": "prop2", "value": "prop2", "start": 60, "end": 73},
{"name": "v-show", "value": "isShow", "start": 74, "end": 89},
{"name": "@click", "value": "clickItem(index)", "start": 90, "end": 115}
],
"attrsMap":{
":class":"classObject",
"class":"list",
"bind:prop1": "prop1",
"prop2": "prop2",
"v-show":"isShow",
"@click":"clickItem(index)"
},
"rawAttrsMap":{},
"children":[]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
首先看一下如下代码:
源码目录:src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
/* 省略... */
}
}
2
3
4
5
6
7
此时传递进来的 el
是经过一系列处理的 AST
树,如下:
{
"type":1,
"tag":"div",
"attrsList":[
{"name":"v-bind:prop1","value":"prop1","start":39,"end":59},
{"name":"prop2","value":"prop2","start":60,"end":73},
{"name":"v-show","value":"isShow","start":74,"end":89},
{"name":"@click","value":"clickItem(index)","start":90,"end":115}
],
"attrsMap":{
":class":"classObject",
"class":"list",
"v-bind:prop1":"prop1",
"prop2":"prop2",
"v-show":"isShow",
"@click":"clickItem(index)"
},
"rawAttrsMap":{
":class":{"name":":class","value":"classObject","start":5,"end":25},
"class":{"name":"class","value":"list","start":26,"end":38},
"v-bind:prop1":{"name":"v-bind:prop1","value":"prop1","start":39,"end":59},
"prop2":{"name":"prop2","value":"prop2","start":60,"end":73},
"v-show":{"name":"v-show","value":"isShow","start":74,"end":89},
"@click":{"name":"@click","value":"clickItem(index)","start":90,"end":115}
},
"children":[
{
"type":2,
"expression":"\" \"+_s(val)+\" \"",
"tokens":[" ",{"@binding":"val"}," "],
"text":" {{ val }} ",
"start":116,
"end":127
}
],
"start":0,
"end":131,
"plain":false,
"staticClass":"\"list\"",
"classBinding":"classObject"
}
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
可以看到在 processAttrs
函数内部,首先定义了 list
常量,它是 el.attrsList
数组的引用,然后是通过一个 for
循环遍历 list
,接下来我们看看for
中的代码,如下:
源码目录:src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
/* 省略... */
} else {
/* 省略... */
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
for
里面首先分别为 name
、rawName
以及 value
变量赋了值,其中 name
和 rawName
变量中保存的是属性的名字,而 value
变量中则保存着属性的值,在们这个案例中第一次循环的值为 {"name":"v-bind:prop1","value":"prop1","start":39,"end":59}
,所以 name = rawName ='v-bind:prop1'
,value = 'prop1'
。
接下来是一个 if...else
语句块,首先我们看一下条件语句 dirRE.test(name)
,从前面章节的分析我们知道 dirRE
用来匹配以字符 v-
或 @
或 :
或 .
或 #
开头的字符串,主要作用是检测标签属性名是否是指令。所以此时 dirRE.test('v-bind:prop1')
为 true
执行 if
语句,我们接下来看一下 if
语句里面的代码,如下:
源码目录:src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
// mark element as dynamic
el.hasBindings = true
// modifiers
modifiers = parseModifiers(name.replace(dirRE, ''))
// support .foo shorthand syntax for the .prop modifier
if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {
(modifiers || (modifiers = {})).prop = true
name = `.` + name.slice(1).replace(modifierRE, '')
} else if (modifiers) {
name = name.replace(modifierRE, '')
}
/* 省略... */
} else {
/* 省略... */
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
一个完整的指令包含指令的名称、指令的参数、指令的修饰符以及指令的值,以上代码的作用是用来解析指令中的修饰符。首先既然元素使用了指令,那么该指令的值就是表达式,既然是表达式那就涉及动态的内容,所以此时会在元素描述对象上添加 el.hasBindings
属性,并将其值设置为 true
,标识着当前元素是一个动态的元素。
接着调用 parseModifiers
函数,该函数接收整个指令字符串即 'v-bind:prop1'
作为参数,作用就是解析指令中的修饰符,并将解析结果赋值给 modifiers
变量。在这里 parseModifiers
参数并非是整个指令字符串,而是通过 name.replace(dirRE, '')
替换后的字符串即 bind:prop1
。接下来我们看看那 parseModifiers
的定义:
源码目录:src/compiler/parser/index.js
function parseModifiers (name: string): Object | void {
const match = name.match(modifierRE)
if (match) {
const ret = {}
match.forEach(m => { ret[m.slice(1)] = true })
return ret
}
}
2
3
4
5
6
7
8
parseModifiers
主要做了两件事:
- 通过
modifierRE
匹配修饰分,例如name
为bind:prop1.prop
所以匹配到的结果为['.prop']
,如果匹配失败则会得到null
。 - 遍历
match
数组,从修饰符的第一位开始截取到末尾即得到prop
,然后以prop
为key
值添加到ret
对象并赋值为true
,最后返回ret
,此时的ret
为{ prop: true }
,如果指令字符串中不包含修饰符,则parseModifiers
函数没有返回值,或者说其返回值为undefined
。
我们再回到 processAttrs
继续往下看,接下来由于 系统设置process.env.VBIND_PROP_SHORTHAND=false
所以 if
语句不会执行,这里就不详细说明,我们继续看一下 else
语句,此时 modifiers
的值为 {prop: true}
所以,会通过 name.replace(modifierRE, '')
替换掉 name
中的修饰符,即为 name ='v-bind:prop1'
。
我们继续往下分析源码,接下来是一个 if...else if...else
语句,首先 if
语句的作用是解析 v-bind
指令,源码如下:
源码目录:src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
/* 省略... */
if(bindRE.test(name){ // v-bind
name = name.replace(bindRE, '')
value = parseFilters(value)
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)
}
/* 省略... */
} else if(onRE.test(name)) { // v-on
/* 省略... */
} else { // 对于其他指令的解析
/* 省略... */
}
} else {
/* 省略... */
}
}
}
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
首先我们看一下 if
的条件表达式 bindRE.test(name)
其中 bindRE
用来匹配以字符 :
或字符串 v-bind:
或字符 .
开头的字符串,在们上面例子中 name
是 v-bind:prop1
所以能匹配到,则执行 if
里面的代码。
首先使用 bindRE
正则将指令字符串中的 v-bind:
或 :
去除掉,此时 name
字符串已经从一个完整的指令字符串变为绑定属性的名字了,例如 prop1
。
接着调用 parseFilters
函数解析过滤器,关于 parseFilters
我们在分析 processKey
的时候已经做了详细分析,请查看上面processKey
小节,在我们这个案例中没有过滤器所以解析到的 value = prop1
,如果我们有过滤器,例如 prop1 |filter1|filter2(a,b)
,此时解析到的 value
为 _f("filter2")(_f("filter1")(prop1), a, b)
。
接下来用 dynamicArgRE
匹配以字符 [
开头并以字符 ]
结尾的字符串,作用是判断是否为动态属性,在我们当前案例中,此时的 name
为 prop1
所以不是动态属性,即 isDynamic = false
。那什么时候是动态属性呢?我们举个例子,例如我们这样绑定属性 v-bind:[name]='name'
,此时获取到的 name
为 [name]
即动态属性,此时 isDynamic = true
。
最后如果是动态 [name]
,通过 slice
从第二位开始截取一直到倒数第二位,最终得到 name
。
我们继续往下看源码,如下:
源码目录:src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
/* 省略... */
if (
process.env.NODE_ENV !== 'production' &&
value.trim().length === 0
) {
warn(
`The value for a v-bind expression cannot be empty. Found in "v-bind:${name}"`
)
}
/* 省略... */
} else if(onRE.test(name)) { // v-on
/* 省略... */
} else { // 对于其他指令的解析
/* 省略... */
}
} else {
/* 省略... */
}
}
}
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
这个if
语句的作用是在开发环境中,如果属性的值为空,则报警告。
我们继续往下看源码,如下:
源码目录:src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
/* 省略... */
if (modifiers) {
if (modifiers.prop && !isDynamic) {
name = camelize(name)
if (name === 'innerHtml') name = 'innerHTML'
}
/* 省略... */
}
/* 省略... */
} else if(onRE.test(name)) { // v-on
/* 省略... */
} else { // 对于其他指令的解析
/* 省略... */
}
} else {
/* 省略... */
}
}
}
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
接下来又是一个 if
语句,在这里的作用主要是处理修饰符,我们先来看一下对修饰符 prop
的处理,在分析之前我们要对我们的案例中 v-bind:prop1='prop1'
做一点修改即加一个 prop
修饰符,例如:v-bind:prop1.prop='prop1'
。接下来我们从前面的分析知道 modifiers = {prop: true}
,isDynamic
为 false
所以条件为真,执行 if
语句 中的 if
语句,首先通过 camelize
函数将属性名驼峰化,最后还会检查驼峰化之后的属性名是否等于字符串 'innerHtml'
,如果属性名全等于该字符串则将属性名重写为字符串 'innerHTML'
,我们知道 'innerHTML'
是一个特例,它的 HTML
四个字符串全部为大写。
我们继续往下看,如下:
源码目录:src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
/* 省略... */
if (modifiers) {
/* 省略... */
if (modifiers.camel && !isDynamic) {
name = camelize(name)
}
/* 省略... */
}
/* 省略... */
} else if(onRE.test(name)) { // v-on
/* 省略... */
} else { // 对于其他指令的解析
/* 省略... */
}
} else {
/* 省略... */
}
}
}
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
这个 if
语句是对修饰符 camel
的处理,如果 modifiers.camel
为真,则说明该绑定的属性使用了 camel
修饰符,使用该修饰符的作用只有一个,那就是将绑定的属性驼峰化。例如 v-bind:prop-data='prop'
,转换后 name
为 propData
。
接着我们来看一下对于最后一个修饰符的处理,即 sync
修饰符,如下:
源码目录:src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
/* 省略... */
if (modifiers) {
/* 省略... */
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 {
// handler w/ dynamic event name
addHandler(
el,
`"update:"+(${name})`,
syncGen,
null,
false,
warn,
list[i],
true // dynamic
)
}
}
}
/* 省略... */
} else if(onRE.test(name)) { // v-on
/* 省略... */
} else { // 对于其他指令的解析
/* 省略... */
}
} else {
/* 省略... */
}
}
}
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
sync
修饰符实际上是一个语法糖,子组件不能够直接修改 prop
值,通常我们会在子组件中发射一个自定义事件,然后在父组件层面监听该事件并由父组件来修改状态。这个过程有时候过于繁琐,为了简化该过程,我们可以在绑定属性时使用 sync
修饰符,例如:
<child :some-prop.sync="value" />
这句代码等价于:
<template>
<child :some-prop="value" @update:someProp="handleEvent" />
</template>
<script>
export default {
data () {
value: ''
},
methods: {
handleEvent (val) {
this.value = val
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
注意事件名称 update:someProp
是固定的,它由 update:
加上驼峰化的绑定属性名称组成。所以在子组件中你需要发射一个名字叫做 update:someProp
的事件才能使 sync
修饰符生效,不难看出这大大提高了开发者的开发效率。
在 Vue
内部,使用 sync
修饰符的绑定属性与没有使用 sync
修饰符的绑定属性之间差异就在于:使用了 sync
修饰符的绑定属性等价于多了一个事件侦听,并且事件名称为 'update:${驼峰化的属性名}'
。
在分析之前我们要对我们的案例中 v-bind:prop1='prop1'
做一点修改即加一个 sync
修饰符,例如:v-bind:prop1.sync='prop1'
。
首先通过调用 genAssignmentCode
函数获取一个代码字符串,源码如下:
源码目录:src/compiler/directives/model.js
export function genAssignmentCode (
value: string,
assignment: string
): string {
const res = parseModel(value)
if (res.key === null) {
return `${value}=${assignment}`
} else {
return `$set(${res.exp}, ${res.key}, ${assignment})`
}
}
2
3
4
5
6
7
8
9
10
11
genAssignmentCode
函数将会牵扯到很多东西,实际上 genAssignmentCode
函数也被用在 v-model
指令,因为本质上 v-model
指令与绑定属性加上 sync
修饰符几乎相同,所以我们会在讲解 v-model
指令时再来详细讲解 genAssignmentCode
函数。
通过genAssignmentCode
我们获得到的 syncGen
字符串为 prop1=$event
,接下来通过调用 addHandler
函数,在当前元素描述对象上添加事件侦听器。addHandler
函数的作用实际上就是将事件名称与该事件的侦听函数添加到元素描述对象的 el.events
属性或 el.nativeEvents
属性中。
此时有三种情况:
- 不是动态属性的情况
- 绑定的
name
是连接符的情况即prop-data
- 动态属性情况
对于 addHandler
函数的实现我们将会在即将讲解的 v-on
指令的解析中为大家详细说明。
接着我们继续往下看,如下:
源码目录:src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
/* 省略... */
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)
}
} else if(onRE.test(name)) { // v-on
/* 省略... */
} else { // 对于其他指令的解析
/* 省略... */
}
} else {
/* 省略... */
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
首先modifiers && modifiers.prop
判断修饰符是否存在并且是 prop
修饰符,说明绑定的属性是原生DOM对象的属性。接着 el.component
必须为假,这个条件能够保证什么呢?我们知道 el.component
属性保存的是标签 is
属性的值,如果 el.component
属性为假就能够保证标签没有使用 is
属性。那么为什么需要这个保证呢?这是因为后边的 platformMustUseProp
函数,总结如下:
input,textarea,option,select,progress
这些标签的value
属性都应该使用元素对象的原生的prop
绑定(除了type === 'button'
之外)option
标签的selected
属性应该使用元素对象的原生的prop
绑定input
标签的checked
属性应该使用元素对象的原生的prop
绑定video
标签的muted
属性应该使用元素对象的原生的prop
绑定
可以看到如果满足这些条件,则意味着即使你在绑定以上属性时没有使用 prop
修饰符,那么它们依然会被当做原生DOM
对象的属性。不过我们还是没有解释为什么要保证 !el.component
成立,这是因为 platformMustUseProp
函数在判断的时候需要标签的名字(el.tag
),而 el.component
会在元素渲染阶段替换掉 el.tag
的值。所以如果 el.component
存在则会影响 platformMustUseProp
的判断结果。
最后我们来对 v-bind
指令的解析做一个总结:
- 任何绑定的属性,最终要么会被添加到元素描述对象的
el.attrs
数组中,要么就被添加到元素描述对象的el.props
数组中。 - 对于使用了
.sync
修饰符的绑定属性,还会在元素描述对象的el.events
对象中添加名字为'update:${驼峰化的属性名}'
的事件。
接着我们看一下对v-on
指令的处理,如下:
源码目录:src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
/* 省略... */
} else if(onRE.test(name)) { // v-on
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 { // 对于其他指令的解析
/* 省略... */
}
} else {
/* 省略... */
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
对于 v-on
指令的处理首先用 onRE
正则表达式匹配 name
,如果该指令字符串以 @
或 v-on:
开头,则说明该指令是事件绑定,此时 elseif
语句块内的代码将会被执行,elseif
语句块内首先也是通过 onRE
正则表达式替换 @
或 v-on:
获取到指令名称,例如 @click="clickItem(index)
所以获取到的 name
为 click
,接下来判断 name
是否为动态属性,如果是则 去掉 []
中括号,最后通过调用 addHandler
函数,在当前元素描述对象上添加事件侦听器。
下面我们看一下 addHandler
的源码,如下:
源码目录:src/compiler/helpers.js
export function addHandler (
el: ASTElement,
name: string,
value: string,
modifiers: ?ASTModifiers,
important?: boolean,
warn?: ?Function,
range?: Range,
dynamic?: boolean
) {
/* 省略 */
}
2
3
4
5
6
7
8
9
10
11
12
首先我们来分析一下每个参数的含义,如下:
el
:当前元素描述对象name
:绑定属性的名字,即事件名称value
:绑定属性的值,这个值有可能是事件回调函数名字,有可能是内联语句,有可能是函数表达式modifiers
:指令对象important
:可选参数,是一个布尔值,代表着添加的事件侦听函数的重要级别,如果为true
,则该侦听函数会被添加到该事件侦听函数数组的头部,否则会将其添加到尾部warn
:打印警告信息的函数,是一个可选参数range
:范围,即在el.attrsList
中对应的属性值dynamic
:是否是动态属性
分析完参数的作用我们继续看函数体的内容,如下:
源码目录:src/compiler/helpers.js
export function addHandler (
/* 省略 */
) {
modifiers = modifiers || emptyObject
// warn prevent and passive modifier
/* istanbul ignore if */
if (
process.env.NODE_ENV !== 'production' && warn &&
modifiers.prevent && modifiers.passive
) {
warn(
'passive and prevent can\'t be used together. ' +
'Passive handler can\'t prevent default event.',
range
)
}
/* 省略 */
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
首先检测 v-on
指令的修饰符对象 modifiers
是否存在,如果在使用 v-on
指令时没有指定任何修饰符,则 modifiers
的值为 undefined
,此时会使用冻结的空对象 emptyObject
作为替代。接着是一个 if
条件语句块,如果该 if
语句的判断条件成立,则说明开发者同时使用了 prevent
修饰符和 passive
修饰符,此时如果是在非生产环境下并且 addHandler
函数的第六个参数 warn
存在,则使用 warn
函数打印警告信息,提示开发者 passive
修饰符不能和 prevent
修饰符一起使用,这是因为在事件监听中 passive
选项参数就是用来告诉浏览器该事件监听函数是不会阻止默认行为的。
再往下是这样一段代码:
源码目录:src/compiler/helpers.js
export function addHandler (
/* 省略 */
) {
/* 省略 */
// normalize click.right and click.middle since they don't actually fire
// this is technically browser-specific, but at least for now browsers are
// the only target envs that have right/middle clicks.
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'
}
}
/* 省略 */
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
这段代码用来规范化“右击”事件和点击鼠标中间按钮的事件,我们知道在浏览器中点击右键一般会出来一个菜单,这本质上是触发了 contextmenu
事件。而 Vue
中定义“右击”事件的方式是为 click
事件添加 right
修饰符。所以如上代码中首先检查了事件名称是否是 click
,如果事件名称是 click
并且使用了 right
修饰符,则会将事件名称重写为 contextmenu
,同时使用 delete
操作符删除 modifiers.right
属性。类似地在 Vue
中定义点击滚轮事件的方式是为 click
事件指定 middle
修饰符,但我们知道鼠标本没有滚轮点击事件,一般我们区分用户点击的按钮是不是滚轮的方式是监听 mouseup
事件,然后通过事件对象的 event.button
属性值来判断,如果 event.button === 1
则说明用户点击的是滚轮按钮。
首先判断修饰符right
如果存在,就执行 if
语句,if
里面又是一个 if...else
语句块,我们以一个例子来说明, 例如 @click.right='handleClick'
,先是判断是否为动态属性,如果是,此时 name
为 name = "(click)==='click'?'contextmenu':'click'"
,如果name
为 click
则直接给 name
赋值 contextmenu
并且删除 right
修饰符。
接着判断修饰符middle
如果存在,就执行 if
语句,if
里面又是一个 if...else
语句块,我们以一个例子来说明, 例如 @click.middle='handleClick'
,先是判断是否为动态属性,如果是,此时 name
为 name = "(click)==='click'?'mouseup':'click'"
,如果name
为 click
则直接给 name
赋值 mouseup
。
我们继续往下看,源码如下:
源码目录:src/compiler/helpers.js
export function addHandler (
/* 省略 */
) {
/* 省略 */
// check capture modifier
if (modifiers.capture) {
delete modifiers.capture
name = prependModifierMarker('!', name, dynamic)
}
if (modifiers.once) {
delete modifiers.once
name = prependModifierMarker('~', name, dynamic)
}
/* istanbul ignore if */
if (modifiers.passive) {
delete modifiers.passive
name = prependModifierMarker('&', name, dynamic)
}
/* 省略 */
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这段代码是处理修饰符 capture
(捕获) 、 once
(只触发一次回调) 、passive
(不阻止默认行为),所以这段代码主要处理一下几件事:
- 如果存在
capture
修饰符, 首先删除capture
属性,再判断如果是动态属性则name
为,例如_p(click,!)
,如果不是动态属性则name
为, 例如!click
- 如果存在
once
修饰符, 首先删除once
属性,再判断如果是动态属性则name
为,例如_p(click,~)
,如果不是动态属性则name
为, 例如~click
- 如果存在
passive
修饰符, 首先删除passive
属性,再判断如果是动态属性则name
为,例如_p(click,&)
,如果不是动态属性则name
为, 例如&click
我们继续往下看,源码如下:
源码目录:src/compiler/helpers.js
export function addHandler (
/* 省略 */
) {
/* 省略 */
let events
if (modifiers.native) {
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else {
events = el.events || (el.events = {})
}
/* 省略 */
}
2
3
4
5
6
7
8
9
10
11
12
13
这段代码是对native
修饰符做处理,首先定义了 events
变量,然后判断是否存在 native
修饰符,如果 native
修饰符存在则会在元素描述对象上添加 el.nativeEvents
属性,初始值为一个空对象,并且 events
变量与 el.nativeEvents
属性具有相同的引用,另外大家注意如上代码中使用 delete
操作符删除了 modifiers.native
属性,到目前为止我们在讲解 addHandler
函数时已经遇到了很多次使用 delete
操作符删除修饰符对象属性的做法,那这么做的目的是什么呢?这是因为在代码生成阶段会使用 for...in
语句遍历修饰符对象,然后做一些相关的事情,所以在生成 AST
阶段把那些不希望被遍历的属性删除掉,更具体的内容我们会在代码生成中为大家详细讲解。回过头来,如果 native
属性不存在则会在元素描述对象上添加 el.events
属性,它的初始值也是一个空对象,此时 events
变量的引用将与 el.events
属性相同。
我们继续往下看,源码如下:
源码目录:src/compiler/helpers.js
export function addHandler (
/* 省略 */
) {
/* 省略 */
const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers
}
/* 省略 */
}
2
3
4
5
6
7
8
9
10
首先定义了 newHandler
对象,通过 rangeSetItem
为该对象初始 value
、dynamic
、start
、end
属性,value
属性的值就是 v-on
指令的属性值,start
、end
分别为 v-on
指令在模板中的开始和结束位置。接着是一个 if
条件语句,该 if
语句的判断条件检测了修饰符对象 modifiers
是否不等于 emptyObject
,我们知道当一个事件没有使用任何修饰符时,修饰符对象 modifiers
会被初始化为 emptyObject
,所以如果修饰符对象 modifiers
不等于 emptyObject
则说明事件使用了修饰符,此时会把修饰符对象赋值给 newHandler.modifiers
属性。
例如 @click.once="clickItem(index)"
,转换成 el.attrsList
属性为:
{ name: "@click.once", value: "clickItem(index)", start: 94, end: 124 }
再经过 rangeSetItem
生成的 newHandler
为:
{ dynamic: false, end: 124, modifiers: {}, start: 94, value: "clickItem(index)"}
我们再来看一下最后一段代码,源码如下:
源码目录:src/compiler/helpers.js
export function addHandler (
/* 省略 */
) {
/* 省略 */
const handlers = events[name]
/* istanbul ignore if */
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
}
el.plain = false
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
首先定义了 handlers
常量,它的值是通过事件名称获取 events
对象下的对应的属性值得到的:events[name]
,我们知道变量 events
要么是元素描述对象的 el.nativeEvents
属性的引用,要么就是元素描述对象 el.events
属性的引用。无论是谁的引用,在初始情况下 events
变量都是一个空对象,所以在第一次调用 addHandler
时 handlers
常量是 undefined
,这就会导致接下来的代码中 else
语句块将被执行。
可以看到在 else
语句块内,为 events
对象定义了与事件名称相同的属性,并以 newHandler
对象作为属性值。
我们以上面的例子 @click.once="clickItem(index)"
来分析,最终生成的 newHandler
为:
// 注意这里是空对象,因为 modifiers.once 修饰符被 delete 了
{ dynamic: false, end: 124, modifiers: {}, start: 94, value: "clickItem(index)"}
2
又因为使用了 once
修饰符,所以事件名称将变为字符串 '~click'
,又因为在监听事件时没有使用 native
修饰符,所以 events
变量是元素描述对象的 el.events
属性的引用,所以调用 addHandler
函数的最终结果就是在元素描述对象的 el.events
对象中添加相应事件的处理结果,如下:
el.event = {
'~click': { dynamic: false, end: 124, modifiers: {}, start: 94, value: "clickItem(index)"}
}
2
3
现在我们来修改一下之前的模板,如下:
<div @click.prevent="handleClick1" @click="handleClick2"></div>
如上模板所示,我们有两个 click
事件的侦听,其中一个 click
事件使用了 prevent
修饰符,而另外一个 click
事件则没有使用修饰符,所以这两个 click
事件是不同,但这两个事件的名称却是相同的,都是 'click'
,所以这将导致调用两次 addHandler
函数添加两次名称相同的事件,但是由于第一次调用 addHandler
函数添加 click
事件之后元素描述对象的 el.events
对象已经存在一个 click
属性,如下:
el.events = {
click: {
value: 'handleClick1',
modifiers: { prevent: true }
}
}
2
3
4
5
6
所以当第二次调用 addHandler
函数时, elseif
语句块的代码将被执行。
此时 newHandler
对象是第二个 click
事件侦听的信息对象,而 handlers
常量保存的则是第一次被添加的事件信息,我们看如上高亮的那句代码,这句代码检测了参数 important
的真假,根据 important
参数的不同,会重新为 events[name]
赋值。可以看到 important
参数的真假所影响的仅仅是被添加的 handlers
对象的顺序。最终元素描述对象的 el.events.click
属性将变成一个数组,这个数组保存着前后两次添加的 click
事件的信息对象,如下:
el.events = {
click: [
{
value: 'handleClick1',
modifiers: { prevent: true }
},
{
value: 'handleClick2'
}
]
}
2
3
4
5
6
7
8
9
10
11
这还没完,我们再次尝试修改我们的模板:
<div @click.prevent="handleClick1" @click="handleClick2" @click.self="handleClick3"></div>
我们在上一次修改的基础上添加了第三个 click
事件侦听,但是我们使用了 self
修饰符,所以这个 click
事件与前两个 click
事件也是不同的, if
语句块的代码将被执行。
由于此时 el.events.click
属性已经是一个数组,所以如上 if
语句的判断条件成立。在 if
语句块内执行了一句代码,这句代码是一个三元运算符,其作用很简单,我们知道 important
所影响的就是事件作用的顺序,所以根据 important
参数的不同,会选择使用数组的 unshift
方法将新添加的事件信息对象放到数组的头部,或者选择数组的 push
方法将新添加的事件信息对象放到数组的尾部。这样无论你有多少个同名事件的监听,都不会落下任何一个监听函数的执行。
接着我们注意到 addHandler
函数的最后一句代码,如下:
el.plain = false
如果一个标签存在事件侦听,无论如何都不会认为这个元素是“纯”的,所以这里直接将 el.plain
设置为 false
。el.plain
属性会影响代码生成阶段,并间接导致程序的执行行为,我们后面会总结一个关于 el.plain
的变更情况,让大家充分地理解。
以上就是对于 addHandler
函数的讲解,我们发现 addHandler
函数对于元素描述对象的影响主要是在元素描述对象上添加了 el.events
属性和 el.nativeEvents
属性。对于 el.events
属性和 el.nativeEvents
属性的结构我们前面已经讲解得很详细了,这里不再做总结。
讲解完了对于 v-on
指令的解析,接下来我们进入如下这段代码:
源码目录:src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
/* 省略... */
} else if(onRE.test(name)) { // v-on
/* 省略... */
} else { // 对于其他指令的解析
name = name.replace(dirRE, '')
// parse arg
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])
if (process.env.NODE_ENV !== 'production' && name === 'model') {
checkForAliasModel(el, value)
}
}
} else {
/* 省略... */
}
}
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
首先我们总结一下所有 `Vue` 内置提供的指令与已经处理过的指令和剩余未处理指令,如下表格:
Vue 内置提供的所有指令 | 是否已经被解析 | 解析函数 |
---|---|---|
v-if | 是 | processIf |
v-else-if | 是 | processIf |
v-else | 是 | processIf |
v-for | 是 | processFor |
v-on | 是 | processAttrs |
v-bind | 是 | processAttrs |
v-pre | 是 | processPre |
v-once | 是 | processOnce |
v-text | 否 | 无 |
v-html | 否 | 无 |
v-show | 否 | 无 |
v-cloak | 否 | 无 |
v-model | 否 | 无 |
通过如上表格可以看到,到目前为止还有五个指令没有得到处理,分别是 v-text
、v-html
、v-show
、v-cloak
以及 v-model
,除了这五个 Vue
内置提供的指令之外,开发者还可以自定义指令,所以上面代码中 else
语句块内的代码就是用来处理剩余的这五个内置指令和其他自定义指令的。
我们回到 else
语句块内的代码,首先使用字符串的 replace
方法配合 dirRE
正则去掉属性名称中的 'v-'
或 ':'
或 '@'
或 '#'
等字符,并重新赋值 name
变量,所以此时 name
变量应该只包含属性名字,假如我们在一个标签中使用 v-show
指令,则此时 name
变量的值为字符串 'show'
。但是对于自定义指令,开发者很可能为该指令提供参数,假设我们有一个叫做 v-custom
的指令,并且我们在使用该指令时为其指定了参数:v-custom:arg
,这时重新赋值后的 name
变量应该是字符串 'custom:arg'
。可能大家会问:如果指令有修饰符那是不是 name
变量保存的字符串中也包含修饰符?不会的,大家别忘了在 processAttrs
函数中每解析一个指令时都优先使用 parseModifiers
函数将修饰符解析完毕了,并且修饰符相关的字符串已经被移除,所以如上代码中的 name
变量中将不会包含修饰符字符串。
接下来使用 argRE
正则匹配变量 name
,并将匹配结果保存在 argMatch
常量中,由于使用的是 match
方法,所以如果匹配成功则会返回一个结果数组,匹配失败则会得到 null
。argRE
正则用来匹配指令字符串中的参数部分,并且拥有一个捕获组用来捕获参数字符串,假设现在 name
变量的值为 custom:arg
,则最终 argMatch
常量将是一个数组:
const argMatch = [':arg', 'arg']
可以看到 argMatch
数组中索引为 1
的元素保存着参数字符串。有了 argMatch
数组后将会检测 argMatch
是否存在,如果存在则取 argMatch
数组中索引为 1
的元素作为常量 arg
的值,所以常量 arg
所保存的就是参数字符串。
接下来判断 arg
是否存在,在我们 v-custom:arg
例子中,arg
存在所以执行 if
语句,然后通过 slice
截取字符串获取到指令名称 name
,即去掉参数标示 :arg
的到 name = 'custom'
。
接下来再去判断 name
是否为动态属性,如果是动态属性则截取 []
获得指令名称,并且 isDynamic
设置为 true
。
接下来调用了 addDirective
函数,并传递给该函数八个参数,我们还是举个例子,假设我们的指令为:v-custom:arg.modif="myMethod"
,则最终调用 addDirective
函数时所传递的参数如下 :
// addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
addDirective(
el,
'custom',
'v-custom:arg.modif',
'myMethod',
'arg',
false,
{ modif: true },
{name: "v-custom:arg.modif", value: "myMethod", start: 94, end: 123}
)
2
3
4
5
6
7
8
9
10
11
实际上 addDirective
函数与 addHandler
函数类似,只不过 addDirective
函数的作用是用来在元素描述对象上添加 el.directives
属性的,如下是 addDirective
函数的源码,如下:
源码目录: src/compiler/helpers.js
export function addDirective (
el: ASTElement,
name: string,
rawName: 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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在 addDirective
函数体内,首先判断了元素描述对象的 el.directives
是否存在,如果不存在则先将其初始化一个空数组,然后再使用 push
方法添加一个指令信息对象到 el.directives
数组中,如果 el.directives
属性已经存在,则直接使用 push
方法将指令信息对象添加到 el.directives
数组中,最后将元素描述对象的 el.plain
属性设置为 false
。
我们回到 processAttrs
函数中,继续看代码,在非生产环境下,如果指令的名字为 model
,则会调用 checkForAliasModel
函数,并将元素描述对象和 v-model
属性值作为参数传递,这段代码的作用是什么呢?我们找到 checkForAliasModel
函数,如下:
源码目录:src/compiler/parser/index.js
function checkForAliasModel (el, value) {
let _el = el
while (_el) {
if (_el.for && _el.alias === value) {
warn(
`<${el.tag} v-model="${value}">: ` +
`You are binding v-model directly to a v-for iteration alias. ` +
`This will not be able to modify the v-for source array because ` +
`writing to the alias is like modifying a function local variable. ` +
`Consider using an array of objects and use v-model on an object property instead.`,
el.rawAttrsMap['v-model']
)
}
_el = _el.parent
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
checkForAliasModel
函数的作用就是从使用了 v-model
指令的标签开始,逐层向上遍历父级标签的元素描述对象,直到根元素为止。并且在遍历的过程中一旦发现这些标签的元素描述对象中存在满足条件:_el.for && _el.alias === value
的情况,就会打印警告信息。
如果这个条件成立,则说明使用了 v-model
指令的标签或其父代标签使用了 v-for
指令,如下:
<div v-for="item of list">
<input v-model="item" />
</div>
2
3
假设如上代码中的 list
数组如下:
[1, 2, 3]
此时将会渲染三个输入框,但是当我们修改输入框的值时,这个变更是不会体现到 list
数组的,换句话说如上代码中的 v-model
指令无效,为什么无效呢?这与 v-for
指令的实现有关,如上代码中的 v-model
指令所执行的修改操作等价于修改了函数的局部变量,这当然不会影响到真正的数据。为了解决这个问题,Vue
也给了我们一个方案,那就是使用对象数组替代基本类型值的数组,并在 v-model
指令中绑定对象的属性,我们修改一下上例并使其生效:
<div v-for="obj of list">
<input v-model="obj.item" />
</div>
2
3
此时在定义 list
数组时,应该将其定义为:
[
{ item: 1 },
{ item: 2 },
{ item: 3 },
]
2
3
4
5
所以实际上 checkForAliasModel
函数的作用就是给开发者合适的提醒。
以上就是对自定义指令和剩余的五个未被解析的内置指令的处理,可以看到每当遇到一个这样的指令,都会在元素描述对象的 el.directives
数组中添加一个指令信息对象。
讲解完了对于其他指令的解析,接下来我们进入如下这段代码:
源码目录:src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
/* 省略... */
} else if(onRE.test(name)) { // v-on
/* 省略... */
} else { // 对于其他指令的解析
/* 省略... */
}
} else {
// literal attribute
if (process.env.NODE_ENV !== 'production') {
const res = parseText(value, delimiters)
if (res) {
warn(
`${name}="${value}": ` +
'Interpolation inside attributes has been removed. ' +
'Use v-bind or the colon shorthand instead. For example, ' +
'instead of <div id="{{ val }}">, use <div :id="val">.',
list[i]
)
}
}
addAttr(el, name, JSON.stringify(value), list[i])
// #6887 firefox doesn't update muted state if set via attribute
// even immediately after element creation
if (!el.component &&
name === 'muted' &&
platformMustUseProp(el.tag, el.attrsMap.type, name)) {
addProp(el, name, 'true', list[i])
}
}
}
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
这块代码的作用就是用来处理非指令属性的,如下列出的非指令属性是我们在之前的讲解中已经讲过的属性:
key
ref
slot
、slot-scope
、scope
、name
is
、inline-template
这些非指令属性都已经被相应的处理函数解析过了,所以 processAttrs
函数是不负责处理如上这些非指令属性的。换句话说除了以上这些以外,其他的非指令属性基本都由 processAttrs
函数来处理,比如 id
、width
等,如下:
<div id="div1" :class="classObject" class="list" prop2="prop2">
{{ val }}
</div>
2
3
如上 div
标签中的 id
属性和 width
属性都会被 processAttrs
函数处理,注意 class
不会被processAttrs
处理,是通过前面的 transforms[i](element, options)
进行处理的 。
首先在非生产环境下才会执行该 if
语句块内的代码,在该 if
语句块内首先调用了 parseText
函数,parseText
函数的作用是用来解析字面量表达式的,什么是字面量表达式呢?如下模板代码所示:
<div id="{{ isTrue ? 'a' : 'b' }}"></div>
其中字符串 "b"
就称为字面量表达式,此时就会使用 parseText
函数来解析这段字符串。如果使用 parseText
函数能够成功解析某个非指令属性的属性值字符串,则说明该非指令属性的属性值使用了字面量表达式,就如同上面的模板中的 id
属性一样。此时将会打印警告信息,提示开发者使用绑定属性作为替代,如下:
<div :id="isTrue ? 'a' : 'b'"></div>
我们往下继续看代码,接下来是 执行 addAttr
函数,将该属性与该属性对应的字符串值添加到元素描述对象的 el.attrs
或 el.dynamicAttrs
数组中。
最后对muted
的兼容性处理,实际上元素描述对象的 el.attrs
数组中所存储的任何属性都会在由虚拟DOM
创建真实DOM
的过程中使用 setAttribute
方法将属性添加到真实DOM
元素上,而在火狐浏览器中存在无法通过DOM元素的 setAttribute
方法为 video
标签添加 muted
属性的问题,所以如上代码就是为了解决该问题的,其方案是如果一个属性的名字是 muted
并且该标签满足 platformMustUseProp
函数(video
标签满足),则会额外调用 addProp
函数将属性添加到元素描述对象的 el.props
数组中。为什么这么做呢?这是因为元素描述对象的 el.props
数组中所存储的任何属性都会在由虚拟DOM创建真实DOM的过程中直接使用真实DOM对象添加,也就是说对于 <video>
标签的 muted
属性的添加方式为:videoEl.muted = true
。另外如上代码的注释中已经提供了相应的 issue
号:#6887
,感兴趣的同学可以去看一下。
# 2.16 processElement
在分析 closeElement
是我们预留了一个 processElement
没有分析,接下来我们看一下 processElement
的定义,源码如下:
源码目录:src/compiler/parser/index.js
export function processElement (
element: ASTElement,
options: CompilerOptions
) {
processKey(element)
// determine whether this is a plain element after
// removing structural attributes
element.plain = (
!element.key && //如果没有key
!element.scopedSlots && //也没有作用域插槽
!element.attrsList.length // 也没有属性
)
processRef(element)
processSlotContent(element)
processSlotOutlet(element)
processComponent(element)
for (let i = 0; i < transforms.length; i++) {
element = transforms[i](element, options) || element
}
processAttrs(element)
return element
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
前面我们分析了 processElement
函数中的一系列 processXXX
函数,还有两段代码没有分析,首先是:
element.plain = (
!element.key && //如果没有key
!element.scopedSlots && //也没有作用域插槽
!element.attrsList.length // 也没有属性
)
2
3
4
5
这句代码的作用是:当结构化的属性(structural attributes
)被移除之后,检查该元素是否是“纯”的。
我们知道 v-for
、v-if/v-else-if/v-else
、v-once
等指令会被认为是结构化的指令(structural directives
)。这些指令在经过 processFor
、processIf
以及 processOnce
等函数处理之后,会把这些指令从元素描述对象的 attrsList
数组中移除。
这段代码判断了元素描述对象的 key
属性是否存在,并且判断元素描述对象的 scopedSlots
属性是否存在,同时检查了元素描述对象的 attrsList
数组是否为空。通过如上条件可知,只有当标签没有使用 key
和 scopedSlots
属性,并且标签没有使用了结构化指令的情况下才被认为是“纯”的,此时会将元素描述对象的 plain
属性设置为 true
。我们暂且记住这一点,当后面讲解静态优化和代码生成时我们会看到 plain
属性的作用。
最后是还有一个 for
循环,如下:
for (let i = 0; i < transforms.length; i++) {
element = transforms[i](element, options) || element
}
2
3
使用一个 for
循环遍历了 transforms
数组,并执行数组中的 transformNode
函数。
说明:关于 transformNode
我们会在后面的章节中做详细分析。
transformNode
定义在 src/platforms/web/compiler/modules/style.js
中
# 2.17 preTransformNode
我们在 第十一章 (opens new window) 中 已经分析过 preTransforms
最终为 :
[ preTransformNode ]
数组中只有一个元素 preTransformNode
,接下来我们逐段来分析 preTransformNode
首先看一下下面代码,如下:
源码目录:src/platforms/web/compiler/modules/model.js
function preTransformNode (el: ASTElement, options: CompilerOptions) {
if (el.tag === 'input') {
const map = el.attrsMap
if (!map['v-model']) {
return
}
/* 省略... */
}
}
2
3
4
5
6
7
8
9
preTransformNode
函数接收两个参数,第一个参数是要预处理的元素描述对象,第二个参数则是编译器的选项参数。
首先判断标签是不是 input
,只有当前解析的标签是 input
标签时才会执行预处理,接下来获取 el.attrsMap
复制给 map
。继续判断input
标签没有使用 v-model
属性,则函数直接返回,什么都不做。所以我们可以说 preTransformNode
函数要预处理的是 使用了 v-model
属性的 input
标签,我们继续看如下代码:
源码目录:src/platforms/web/compiler/modules/model.js
function preTransformNode (el: ASTElement, options: CompilerOptions) {
if (el.tag === 'input') {
/* 省略... */
let typeBinding
if (map[':type'] || map['v-bind:type']) {
typeBinding = getBindingAttr(el, 'type')
}
if (!map.type && !typeBinding && map['v-bind']) {
typeBinding = `(${map['v-bind']}).type`
}
/* 省略... */
}
}
2
3
4
5
6
7
8
9
10
11
12
13
在分析之前我们还是用一个案例来具体说明,如下:
<input v-model="val" :type="inputType" />
首先判断标签有没有通过 :
或 v-bind:
绑定 type
属性,如果存在其一,则使用 getBindingAttr
函数获取绑定的 type
属性的值。
接下来如果该 if
条件语句的判断条件成立,则说明该 input
标签没有使用非绑定的 type
属性,并且也没有使用 v-bind:
或 :
绑定 type
属性,并且开发者使用了 v-bind
,但仍然可以通过如下方式绑定属性:
<input v-model="val" v-bind="{ type: inputType }" />
此时就需要通过读取绑定对象的 type
属性来获取绑定的属性值。
总之我们要想方设法获取到绑定的 type
属性的值,如果获取不到则说明该 input
标签的类型是固定不变的,因为它是非绑定的。只有当一个 input
表单拥有绑定的 type
属性时才会执行真正的预处理代码,所以现在我们可以进一步的总结:preTransformNode
函数要预处理的是使用了 v-model
属性并且使用了绑定的 type
属性的 input
标签。
那么要如何处理使用了 v-model
属性并且使用了绑定的 type
属性的 input
标签呢?来看一下 model.js
文件开头的一段注释:
/**
* Expand input[v-model] with dyanmic type bindings into v-if-else chains
* Turn this:
* <input v-model="data[type]" :type="type">
* into this:
* <input v-if="type === 'checkbox'" type="checkbox" v-model="data[type]">
* <input v-else-if="type === 'radio'" type="radio" v-model="data[type]">
* <input v-else :type="type" v-model="data[type]">
*/
2
3
4
5
6
7
8
9
根据如上注释可知 preTransformNode
函数会将形如:
<input v-model="data[type]" :type="type">
这样的 input
标签扩展为如下三种 input
标签:
<input v-if="type === 'checkbox'" type="checkbox" v-model="data[type]">
<input v-else-if="type === 'radio'" type="radio" v-model="data[type]">
<input v-else :type="type" v-model="data[type]">
2
3
我们知道在 AST
中一个标签对应一个元素描述对象,所以从结果上看,preTransformNode
函数将一个 input
元素描述对象扩展为三个 input
标签的元素描述对象。但是由于扩展后的标签由 v-if
、v-else-if
和 v-else
三个条件指令组成,我们在前面的分析中得知,对于使用了 v-else-if
和 v-else
指令的标签,其元素描述对象是会被添加到那个使用 v-if
指令的元素描述对象的 el.ifConditions
数组中的。所以虽然把一个 input
标签扩展成了三个,但实际上并不会影响 AST
的结构,并且从渲染结果上看,也是一致的。
但为什么要将一个 input
标签扩展为三个呢?这里有一个重要因素,由于使用了绑定的 type
属性,所以该 input
标签的类型是不确定的,我们知道同样是 input
标签,但类型为 checkbox
的 input
标签与类型为 radio
的 input
标签的行为是不一样的。到代码生成的阶段大家会看到正是因为这里将 input
标签类型做了区分,才使得代码生成时能根据三种不同情况生成三种对应的代码,从而实现三种不同的功能。有的同学就会问了,这里不做区分可不可以?答案是可以,但是假如这里不做区分,那么当你在代码生成时是不可能知道目标 input
元素的类型是什么的,为了保证实现所有类型 input
标签的功能可用,所以你必须保证生成的代码能完成所有类型标签的工作。换句话说你要么选择在编译阶段区分类型,要么就在运行时阶段区分类型。而 Vue
选择了在编译阶段就将类型区分开来,这么做的好处是运行时的代码在针对某种特定类型的 input
标签时所执行的代码是很单一职责的。当我们后面分析代码生成时你同样能够看到,在编译阶段区分类型使得代码编写更加容易。如果从另外一个角度来讲,由于不同类型的 input
标签所绑定的事件未必相同,所以这也是在编译阶段区分 input
标签类型的一个重要因素。
我们继续往下看,源码如下:
源码目录:src/platforms/web/compiler/modules/model.js
function preTransformNode (el: ASTElement, options: CompilerOptions) {
if (el.tag === 'input') {
/* 省略... */
if(typeBinding) {
const ifCondition = getAndRemoveAttr(el, 'v-if', true)
const ifConditionExtra = ifCondition ? `&&(${ifCondition})` : ``
const hasElse = getAndRemoveAttr(el, 'v-else', true) != null
const elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true)
/* 省略... */
}
}
}
2
3
4
5
6
7
8
9
10
11
12
这段代码定义了四个常量,分别是 ifCondition
、ifConditionExtra
、hasElse
以及 elseIfCondition
,其中 ifCondition
常量保存的值是通过 getAndRemoveAttr
函数取得的 v-if
指令的值,注意如上代码中调用 getAndRemoveAttr
函数时传递的第三个参数为 true
,所以在获取到属性值之后,会将该属性从元素描述对象的 el.attrsMap
中移除。
假设我们有如下模板:
<input v-model="val" :type="inputType" v-if="display" />
则 ifCondition
常量的值为字符串 'display'
。
第二个常量 ifConditionExtra
同样是一个字符串,还是以如上模板为例,由于 ifCondition
常量存在,所以 ifConditionExtra
常量的值为字符串 '&&(display)'
,假若 ifCondition
常量不存在,则 ifConditionExtra
常量的值将是一个空字符串。
第三个常量 hasElse
是一个布尔值,它代表着 input
标签是否使用了 v-else
指令。其实现方式同样是通过 getAndRemoveAttr
函数获取 v-else
指令的属性值,然后将值与 null
做比较。如果 input
标签使用 v-else
指令,则 hasElse
常量的值为真,反之为假。
第四个常量 elseIfCondition
与 ifCondition
类似,只不过 elseIfCondition
所存储的是 v-else-if
指令的属性值。
我们继续往下看,源码如下:
源码目录:src/platforms/web/compiler/modules/model.js
function preTransformNode (el: ASTElement, options: CompilerOptions) {
if (el.tag === 'input') {
/* 省略... */
if(typeBinding) {
/* 省略... */
// 1. checkbox
const branch0 = cloneASTElement(el)
// process for on the main node
processFor(branch0)
addRawAttr(branch0, 'type', 'checkbox')
processElement(branch0, options)
branch0.processed = true // prevent it from double-processed
branch0.if = `(${typeBinding})==='checkbox'` + ifConditionExtra
addIfCondition(branch0, {
exp: branch0.if,
block: branch0
})
/* 省略... */
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这段代码的作用就是创建复选按钮的,首先调用 cloneASTElement
函数克隆出一个与原始标签的元素描述对象一模一样的元素描述对象出来,并将新克隆出的元素描述对象赋值给 branch0
常量。
分别调用了 processFor
函数和 processElement
函数,大家应该已经注意到了,这里并没有调用 processOnce
函数以及 processIf
函数,为什么没有调用这两个函数呢?对于 processOnce
函数,既然没有调用该函数,那么就能说明一个问题,即如下代码中的 v-once
指令无效:
<input v-model="val" :type="inputType" v-once />
大家想象一下这样设计是否合理?我认为这是合理的,对于一个既使用了 v-model
指令又使用了绑定的 type
属性的 input
标签而言,难道它还存在静态的意义吗。
除了没有调用 processOnce
函数之外,还没有调用 processIf
函数,这是因为对于条件指令早已经处理完了,如下是我们前面讲解过的代码:
const ifCondition = getAndRemoveAttr(el, 'v-if', true)
const ifConditionExtra = ifCondition ? `&&(${ifCondition})` : ``
const hasElse = getAndRemoveAttr(el, 'v-else', true) != null
const elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true)
2
3
4
实际上 preTransformNode
函数的处理逻辑就是把一个 input
标签扩展为多个标签,并且这些扩展出来的标签彼此之间是互斥的,后面大家会看到这些扩展出来的标签都存在于元素描述对象的 el.ifConditions
数组中。
在 processFor
函数和 processElement
函数中调用了 addRawAttr
函数,作用就是将属性的名和值分别添加到元素描述对象的 el.attrsMap
对象以及 el.attrsList
数组中。
以如下这句话为例:
addRawAttr(branch0, 'type', 'checkbox')
这么做就等价于把新克隆出来的标签视作:
<input type="checkbox" />
接下来把元素描述对象的 el.processed
属性设置为 true
,标识着当前元素描述对象已经被处理过了。
然后为元素描述对象添加了 el.if
属性,我们以上面的案例 <input v-model="val" :type="inputType" v-if="display" />
来说,其 if
属性值为 (${inputType})==='checkbox'&&display
,最后将标签的元素描述对象被添加到其自身的 el.ifConditions
数组中。
我们继续往下看,源码如下:
源码目录:src/platforms/web/compiler/modules/model.js
function preTransformNode (el: ASTElement, options: CompilerOptions) {
if (el.tag === 'input') {
/* 省略... */
if(typeBinding) {
/* 省略... */
// 2. add radio else-if condition
const branch1 = cloneASTElement(el)
getAndRemoveAttr(branch1, 'v-for', true)
addRawAttr(branch1, 'type', 'radio')
processElement(branch1, options)
addIfCondition(branch0, {
exp: `(${typeBinding})==='radio'` + ifConditionExtra,
block: branch1
})
// 3. other
const branch2 = cloneASTElement(el)
getAndRemoveAttr(branch2, 'v-for', true)
addRawAttr(branch2, ':type', typeBinding)
processElement(branch2, options)
addIfCondition(branch0, {
exp: ifCondition,
block: branch2
})
/* 省略... */
}
}
}
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
这段代码的作用就是创建单选按钮和其他类型的 input
标签。都重新使用 cloneASTElement
函数克隆出了新的元素描述对象并且这两个元素描述对象都会被添加到复选按钮元素描述对象的 el.ifConditions
数组中。
需要注意的是单纯的将克隆出来的元素描述对象中的 v-for
属性移除掉,因为在复选按钮中已经使用 processFor
处理过了 v-for
指令,由于它们本是互斥的,其本质上等价于是同一个元素,只是根据不同的条件渲染不同的标签罢了,所以 v-for
指令处理一次就够了。
我们继续往下看,源码如下:
源码目录:src/platforms/web/compiler/modules/model.js
function preTransformNode (el: ASTElement, options: CompilerOptions) {
if (el.tag === 'input') {
/* 省略... */
if(typeBinding) {
/* 省略... */
if (hasElse) {
branch0.else = true
} else if (elseIfCondition) {
branch0.elseif = elseIfCondition
}
return branch0
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
首先判断该标签是否使用了 v-else-if
和 v-else
指令,如果使用则给标签的元素描述对象添加 else
和 elseif
属性,并赋值。最后返回第一个克隆的元素描述对象。
# 2.18 transformNode
理函数 transformNode
的作用是对 class
属性和 style
属性进行扩展。
我们在 第十一章 (opens new window) 中 已经分析过 transforms
最终为 :
[
transformNode, // class
transformNode // style
]
2
3
4
数组中只有两个元素 transformNode
,第一个是的 class
属性的扩展,第二个是对 style
属性的扩展,首先我们来看对class
属性的扩展,源码如下:
源码目录:src/platforms/web/compiler/modules/class.js
function transformNode (el: ASTElement, options: CompilerOptions) {
const warn = options.warn || baseWarn
const staticClass = getAndRemoveAttr(el, 'class')
if (process.env.NODE_ENV !== 'production' && staticClass) {
const res = parseText(staticClass, options.delimiters)
if (res) {
warn(
`class="${staticClass}": ` +
'Interpolation inside attributes has been removed. ' +
'Use v-bind or the colon shorthand instead. For example, ' +
'instead of <div class="{{ val }}">, use <div :class="val">.',
el.rawAttrsMap['class']
)
}
}
if (staticClass) {
el.staticClass = JSON.stringify(staticClass)
}
const classBinding = getBindingAttr(el, 'class', false /* getStatic */)
if (classBinding) {
el.classBinding = classBinding
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
分析之前我们还是找个案例,如下:
<div :class="classObject" class="list" style="color:red;"> {{ val }} </div>
首先定义 warn
常量,它是一个函数,用来打印警告信息。接着使用 getAndRemoveAttr
函数从元素描述对象上获取非绑定的 class
属性的值,并将其保存在 staticClass
常量中。接着进入一段 if
条件语句,在非生产环境下,并且非绑定的 class
属性值存在,则会使用 parseText
函数解析该值,如果解析成功则说明你在非绑定的 class
属性中使用了字面量表达式,例如:
<div class="{{ isActive ? 'active' : '' }}"></div>
这时 Vue
会打印警告信息,提示你使用如下这种方式替代:
<div :class="{ 'active': isActive }"></div>
接下来判断如果非绑定的 class
属性值存在,则将该值保存在元素描述对象的 el.staticClass
属性中,例如 staticClass = '"list"'
,注意这里使用 JSON.stringify
对值做了处理,这么做的目的我们已经说过很多遍了。
接下来使用了 getBindingAttr
函数获取绑定的 class
属性的值,如果绑定的 class
属性的值存在,则将该值保存在 el.classBinding
属性中,例如 el.classBinding = 'classObject'
。
我们再来看对style
属性的扩展,源码如下:
源码目录:src/platforms/web/compiler/modules/style.js
function transformNode (el: ASTElement, options: CompilerOptions) {
const warn = options.warn || baseWarn
const staticStyle = getAndRemoveAttr(el, 'style')
if (staticStyle) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
const res = parseText(staticStyle, options.delimiters)
if (res) {
warn(
`style="${staticStyle}": ` +
'Interpolation inside attributes has been removed. ' +
'Use v-bind or the colon shorthand instead. For example, ' +
'instead of <div style="{{ val }}">, use <div :style="val">.',
el.rawAttrsMap['style']
)
}
}
el.staticStyle = JSON.stringify(parseStyleText(staticStyle))
}
const styleBinding = getBindingAttr(el, 'style', false /* getStatic */)
if (styleBinding) {
el.styleBinding = styleBinding
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
用来处理 style
属性的 transformNode
函数基本与用来处理 class
属性的 transformNode
函数相同。
与 class
属性不同,如果一个标签使用了非绑定的 style
属性,则会使用 parseStyleText
函数对属性值进行处理。在我们这个案例中模板中使用了非绑定的 style
属性,属性值为字符串 'color: red; '
,parseStyleText
函数会把这个字符串解析为对象形式,如下:
{
color: 'red'
}
2
3
然后在使用 JSON.stringify
函数将如上对象变为字符串后赋值给元素描述对象的 el.staticStyle
属性。
最后使用 getBindingAttr
函数获取到绑定的 style
属性值后,如果值存在则直接将其赋值给元素描述对象的 el.styleBinding
属性。
我们接下来看看parseStyleText
的定义,如下:
/**
* 字符串解析为对象形式
* 例如:<div style="color: red; background: green;"></div>
*/
export const parseStyleText = cached(function (cssText) {
const res = {}
// 样式字符串中分号(;)用来作为每一条样式规则的分割
const listDelimiter = /;(?![^(]*\))/g
// 冒号(:)则用来一条样式规则中属性名与值的分割
const propertyDelimiter = /:(.+)/
// 分割字符串,例如:[ 'color: red', 'background: green']
cssText.split(listDelimiter).forEach(function (item) {
if (item) {
// 分割字符串,例如:[ 'color', 'red']
const tmp = item.split(propertyDelimiter)
// 给res添加属性,例如:res['color'] = 'red'
tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim())
}
})
return res
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3. 处理闭合标签
我们来看一下处理闭合标签的源码,如下:
源码目录:src/compiler/parser/index.js
end (tag, start, end) {
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
element.end = end
}
closeElement(element)
}
2
3
4
5
6
7
8
9
10
当解析器遇到非一元标签的开始标签时,会将该标签的元素描述对象设置给 currentParent
变量,代表后续解析过程中遇到的所有标签都应该是 currentParent
变量所代表的标签的子节点,同时还会将该标签的元素描述对象添加到 stack
栈中。而当遇到结束标签的时候则意味着 currentParent
变量所代表的标签以及其子节点全部解析完毕了,此时我们应该把 currentParent
变量的引用修改为当前标签的父标签,这样我们就将作用域还原给了上层节点,以保证解析过程中正确的父子关系。
首先读取 stack
栈中的最后一个元素并赋值给 element
,接着将当前节点出栈:stack.length -= 1
,然后再读取出栈后 stack
栈中的最后一个元素作为 currentParent
变量的值。
接下来调用了 closeElement
函数,closeElement
函数的调用时机有两个,当遇到一元标签或非一元标签的结束标签时都会调用 closeElement
函数。
closeElement
逻辑很简单,就是更新一下inVPre
和inPre
的状态,以及执行 postTransforms
函数。
说明:关于 closeElement
我们在上面小节中已经做了详细分析,请查看 这里 (opens new window)。
# 4. 处理文本内容
老规矩在分析之前我们还是以一个案例来分析,源码如下:
<div style="color:red;">{{ val }} 111</div>
我们先来看一下下面代码,如下:
源码目录:src/compiler/parser/index.js
chars (text: string, start: number, end: number) {
// 判断是否有当前的父节点
if (!currentParent) {
if (process.env.NODE_ENV !== 'production') {
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.',
{ start }
)
} else if ((text = text.trim())) {
warnOnce(
`text "${text}" outside root element will be ignored.`,
{ start }
)
}
}
return
}
/* 省略... */
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
首先判断了 currentParent
变量是否存在,我们知道 currentParent
变量指向的是当前节点的父节点,如果父节点不存在才会执行该 if
条件语句里面的代码。大家思考一下,如果 currentParent
变量不存在说明什么问题?我们知道如果代码执行到了这里,那么当前节点必然是文本节点,并且该文本节点没有父级节点。
有两种情况:
- 第一:模板中只有文本节点
<template>
我是文本节点
</template>
2
3
- 第二:文本节点在根元素的外面
<template>
<div>根元素内的文本节点</div>根元素外的文本节点
</template>
2
3
第一种情况会打印警告信息提示模板不能只是文本,必须有一个元素节点才行;第二种情况会打印警告信息提示开发者根元素外的文本将会被忽略。
我们继续往下看,代码如下:
源码目录:src/compiler/parser/index.js
chars (text: string, start: number, end: number) {
/* 省略... */
// IE textarea placeholder bug
/* istanbul ignore if */
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
/* 省略... */
}
2
3
4
5
6
7
8
9
10
11
12
这段代码是用来解决 IE
浏览器中渲染 <textarea>
标签的 placeholder
属性时存在的 bug
的。具体的问题大家可以点击这个 issue (opens new window) 查看。
为了让大家更好理解,我们举个例子,如下 html
代码所示:
<div id="box">
<textarea placeholder="some placeholder..."></textarea>
</div>
2
3
如上 html
片段存在一个 <textarea>
标签,该标签拥有 placeholder
属性,但却没有真实的文本内容,假如我们使用如下代码获取字符串内容:
document.getElementById('box').innerHTML
在 IE 浏览器中将得到如下字符串:
'<textarea placeholder="some placeholder...">some placeholder...</textarea>'
可以看到 <textarea>
标签的 placeholder
属性的属性值被设置成了 <textarea>
的真实文本内容,为了解决这个问题,会判断当前文本节点的父元素是 <textarea>
标签,并且文本元素的内容和 <textarea>
标签的 placeholder
属性值相同,则说明此时遇到了 IE
的 bug,由于只有当 <textarea>
标签没有真实文本内容时才存在这个 bug
,所以这说明当前解析的文本节点原本就是不存在的,这时 chars
钩子函数会直接 return
,不做后续处理。
我们继续往下看,代码如下:
源码目录:src/compiler/parser/index.js
chars (text: string, start: number, end: number) {
/* 省略... */
const children = currentParent.children
if (inPre || text.trim()) {
text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
} else if (!children.length) {
// remove the whitespace-only node right after an opening tag
text = ''
} else if (whitespaceOption) {
if (whitespaceOption === 'condense') {
// in condense mode, remove the whitespace node if it contains
// line break, otherwise condense to a single space
text = lineBreakRE.test(text) ? '' : ' '
} else {
text = ' '
}
} else {
text = preserveWhitespace ? ' ' : ''
}
/* 省略... */
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
首先定义了 children
常量,它是 currentParent.children
的引用。接着判断了条件 inPre || text.trim()
的真假,如果为真,首先使用 isTextTag
函数检测当前文本节点的父节点是否是文本标签(即 script
标签或 style
标签),如果当前文本节点的父节点是文本标签,那么则原封不动的保留原始文本,否则使用 decodeHTMLCached
函数对文本进行解码。
说明:关于 decodeHTMLCached
我们会在下一小节做详细分析。
接下来判断当前节点有没有子节点,如果没有子节点而且不存在于 <pre>
标签内的空白符则执行 else...if
语句,将 text
变量设置为空字符串,例如:
<div> </div>
接下来判断空白处理策略( 'preserve' | 'condense'
)是否存在,首先我们需要明白一点程序执行到这里的前提是,不存在于 <pre>
标签的空白符,当前节点存在兄弟节点。如果存在则判断是否为 'condense'
,如果是,则通过 lineBreakRE
匹配换行符或回车符,匹配到将 text
变量设置为空字符串,匹配不到将 text
变量设置为' '
,例如:
// lineBreakRE能匹配到换行符或回车符
<div><span></span>
</div>
// lineBreakRE不能能匹配到换行符或回车符
<div><span></span> </div>
2
3
4
5
6
如果空白处理策略不存在,将 text
变量设置为' '
。
最后前面三种情况都没有匹配到则执行 else
语句,接着通过 preserveWhitespace
判断是否保留元素之间的空白,如果为true
说明保留空白即将 text
变量设置为' '
,反之不保留即将 text
变量设置为空字符串。
我们继续往下看,代码如下:
源码目录:src/compiler/parser/index.js
chars (text: string, start: number, end: number) {
/* 省略... */
if (text) {
if (!inPre && whitespaceOption === 'condense') {
// condense consecutive whitespaces into single space
text = text.replace(whitespaceRE, ' ')
}
let res
let child: ?ASTNode
// 包含表达式的text
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
// 纯文本的text
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
child = {
type: 3,
text
}
}
if (child) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
children.push(child)
}
}
}
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
这是一个 if
条件语句,可以看到该条件语句块内的代码只有当 text
变量存在时才会执行,所以当编译器选项 preserveWhitespace
的值为 false
时,所有空白符都会被忽略,从而导致不会执行如上这段 html
代码,所以也就没有空白符节点被创建。
接下来是一系列 if
语句,我们先看第一个,第一个 if
的作用是将连续空白压缩为单个空格,如果文本节点不在pre
表情并且空白处理策略为condense
时,通过 whitespaceRE
匹配任何空白字符一次或多次,包括空格、制表符、换页符等等,并替换成 ' '
。
接下来 if
语句的判断条件为真则说明:
- 1、当前文本节点不存在于使用
v-pre
指令的标签之内 - 2、当前文本节点不是空格字符
- 3、使用
parseText
函数成功解析当前文本节点的内容
在我们当前案例中,该节点的文本内容是字符串:' 111'
,这个字符串并不是普通的字符串,它包含了 Vue
语法中的字面量表达式,而 parseText
函数的作用就是用来解析这段包含了字面量表达式的文本的,如果解析成功则说明该文本节点的内容确实包含字面量表达式,所以此时会执行以下代码创建一个类型为2(type = 2
)的元素描述对象。
说明:关于 parseText
我们会在下一小节做详细分析。
如果 if
语句的判断条件失败,则有三种可能:
- 1、文本节点存在于使用了
v-pre
指令的标签之内 - 2、文本节点是空格字符
- 3、文本节点的文本内容通过
parseText
函数解析失败
只要以上三种情况中,有一种情况出现则代码会来到 else...if
分支的判断。
如果 else...if
语句的判断条件成立,则有以下几种可能:
- 1、文本内容不是空格,即
text !== ' '
- 2、如果文本内容是空格,但是该文本节点的父节点还没有子节点(即
!children.length
),这说明当前文本内容就是父节点的第一个子节点 - 3、如果文本内容是空格,并且该文本节点的父节点有子节点,但最后一个子节点不是空格,此时也会执行
else...if
语句块内的代码
当文本满足以上条件,就会被当做普通文本节点对待,此时会创建类型为3(type = 3
)的元素描述对象,并将其添加到父级节点的子节点中。
最后将该文本节点的元素描述对象添加到父级的子节点中,另外我们注意到类型为 2
的元素描述对象拥有三个特殊的属性,分别是 expression
、tokens
以及 text
,其中 text
就是原始的文本内容,而 expression
和 tokens
的值是通过 parseText
函数解析的结果中读取的。
总结:
- 1、如果文本节点存在于
v-pre
标签中,则会被作为普通文本节点对象 - 2、
<pre>
标签内的空白会被保留 - 3、
preserveWhitespace
只会保留那些不在开始标签之后的空格(说空白也没问题) - 4、普通文本节点的元素描述对象的类型为 3,即
type = 3
- 5、包含字面量表达式的文本节点不会被作为普通的文本节点对待,而是会使用
parseText
函数解析它们,并创建一个类型为 2,即type = 2
的元素描述对象
# 4.1 decodeHTMLCached
对于decodeHTMLCached
函数解码文本,来看如下代码:
<pre>
<div>我是一个DIV</div>
</pre>
2
3
我们通常会使用 <pre>
标签展示源码,所以通常会书写 html
实体,假如不对如上 html
实体进行解码,那么最终展示在页面上的内容就是字符串 '<div>我是一个DIV</div>'
而非 '我是一个DIV'
,这是因为 Vue
在创建文本节点时使用的是 document.createTextNode
函数,这不同于将如上模板直接交给浏览器解析并渲染,所以需要解码后将字符串 '我是一个DIV'
作为一个文本节点创建才行。
# 4.2 paserText
我们来看一下paserText
的源码,如下:
源码目录:src/compiler/parser//text-parser.js
/* @flow */
import { cached } from 'shared/util'
import { parseFilters } from './filter-parser'
// 匹配viwe 视图中的{{指令}}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
// 匹配特殊符号 - 或者. 或者* 或者+ 或者? 或者^ 或者$ 或者{ 或者} 或者( 或者) 或者| 或者[ 或者] 或者/ 或者\
const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g
const buildRegex = cached(delimiters => {
//$&与 regexp 相匹配的子串,这里的意思是遇到了特殊符号的时候在正则里面需要替换加多一个/斜杠
const open = delimiters[0].replace(regexEscapeRE, '\\$&')
const close = delimiters[1].replace(regexEscapeRE, '\\$&')
// 匹配开始的open +任意字符或者换行符+ close 全局匹配
return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
})
type TextParseResult = {
expression: string,
tokens: Array<string | { '@binding': string }>
}
/**
* 解析文本
* @param {文本} text
* @param {被修改默认的标签匹配} delimiters
*/
export function parseText (
text: string,
delimiters?: [string, string]
): TextParseResult | void {
// 如果delimiters不存在则用默认指令 {{}},如果修改成其他指令则用其他指令
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
// 匹配是否有表达式,比如:{{message}} 如果没有,则表示是纯文本节点,则直接返回不做处理
if (!tagRE.test(text)) {
return
}
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
// 用正则tagRE去匹配text,此时match就是text里的每个值,
// 对于:{{item}}:{{index}}来说,
// match等于
// Array["{{item}}","item"] 、
// Array["{{index}}","index"]
while ((match = tagRE.exec(text))) {
// 匹配的字符串在整个字符串中的位置
index = match.index
// push text token
// 如果index大于lastIndex,
// 表明中间还有一段文本,比如:{{item}}:{{index}},
// 中间的:就是文本
if (index > lastIndex) {
// 截取匹配到字符串指令前面的字符串,并添加到rawTokens
rawTokens.push(tokenValue = text.slice(lastIndex, index))
// 添加匹配到字符串指令前面的字符串
tokens.push(JSON.stringify(tokenValue))
}
// tag token
// 调用parseFilters对match[1做解析];
// 例如{{no | a(100) | b }},
// 解析后的格式为:_f("b")(_f("a")(no,100))
const exp = parseFilters(match[1].trim())
// 把指令转义成函数,便于vonde 虚拟dom 渲染
// 比如指令{{name}} 转换成 _s(name)
tokens.push(`_s(${exp})`)
// 绑定指令{{name}} 指令转换成 [{@binding: "name"}]
rawTokens.push({ '@binding': exp })
// 设置下一次开始匹配的位置
lastIndex = index + match[0].length
}
// 截取剩余的普通文本并将其添加到 rawTokens 和 tokens 数组中
if (lastIndex < text.length) {
// 截取字符串到最后一位
rawTokens.push(tokenValue = text.slice(lastIndex))
// 拼接最后一位字符串
tokens.push(JSON.stringify(tokenValue))
}
return {
// 拼凑成一个表达式,例如:"_s(item)+":"+_s(index)"
expression: tokens.join('+'),
// 模板信息,例如[{@binding: "item"},":",{@binding: "index"}]
tokens: rawTokens
}
}
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
# 5. 处理注释标签
我们来看一下处理闭合标签的源码,如下:
源码目录:src/compiler/parser/index.js
comment (text: string, start, end) {
// adding anyting as a sibling to the root node is forbidden
// comments should still be allowed, but ignored
if (currentParent) {
const child: ASTText = {
type: 3,
text,
isComment: true
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
currentParent.children.push(child)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
解析器是否会解析并保留注释节点,是由 shouldKeepComment
编译器选项决定的,开发者可以在创建 Vue
实例的时候通过设置 comments
选项的值来控制编译器的 shouldKeepComment
选项。默认情况下 comments
选项的值为 false
,即不保留注释,假如将其设置为 true
,则当解析器遇到注释节点时会保留该注释节点,此时 parseHTML
函数的 comment
钩子函数会被调用。
comment
钩子函数接收注释节点的内容、开始位置、结束位置作为参数,在 comment
钩子函数内所做的事情很简单,首先判断父级节点是否存在,如果存在就是为当前注释节点创建一个类型为 3
并且 isComment
属性为 true
的元素描述对象,并将其添加到父节点元素描述对象的 children
数组内。
大家需要注意的是,普通文本节点与注释节点的元素描述对象的类型是一样的,都是 3
,不同的是注释节点的元素描述对象拥有 isComment
属性,并且该属性的值为 true
,目的就是用来与普通文本节点作区分的。