# Vue 模板 AST 详解
# type
节点元素描述对象的 type
属性用来标识一个节点的类型。
- 示例:
ast = {
type: 1
}
它有三个可取值,分别是 1
、2
、3
,分别代表的含义是:
1
:代表当前节点类型为标签2
:包含字面量表达式的文本节点3
:普通文本节点或注释节点
# expression
当节点类型为 2
时,该节点的元素描述对象会包含 expression
属性。
- 示例:
ast = {
type: 2,
expression: "'abc'+_s(name)+'def'"
}
# tokens
与 expression
类似,当节点类型为 2
时,该节点的元素描述对象会包含 tokens
属性。
- 示例:
ast = {
type: 2,
expression: "'abc'+_s(name)+'def'",
tokens: [
'abc',
{
'@binding': '_s(name)'
},
'def'
]
}
节点元素描述对象的 tokens
属性是用来给 weex
使用的,这里不做过多解释。
# tag
只有当节点类型为 1
,即该节点为标签时其元素描述对象才会有 tag
属性,该属性的值代表标签的名字。
- 示例:
ast = {
type: 1,
tag: 'div'
}
# attrsList
只有当节点类型为 1
,即该节点为标签时其元素描述对象才会有 attrsList
属性,它是一个对象数组,存储着原始的 html
属性名和值。
- 示例:
ast = {
type: 1,
attrsList: [
{
name: 'v-for',
value: 'obj of list'
},
{
name: 'class',
value: 'box'
}
]
}
# attrsMap
节点元素描述对象的 attrsMap
属性与 attrsList
属性一样,不同点在于 attrsMap
是以键值对的方式保存 html
属性名和值的。
- 示例:
ast = {
type: 1,
attrsMap: {
'v-for': 'obj of list',
'class': 'box'
}
}
# attrs
节点元素描述对象的 attrs
属性也是一个数组,并且也只有当节点类型为 1
,即节点为标签的时候,其元素描述对象才会包含这个属性。attrs
属性不同于 attrsList
属性,具体表现在:
- 1、
attrsList
属性仅用于解析阶段,而attrs
属性则用于代码生成阶段,甚至运行时阶段。 - 2、
attrsList
属性所包含的内容作为元素材料被解析器使用,而attrs
属性所包含的内容在运行时阶段会使用原生DOM
操作方法setAttribute
真正将属性设置给DOM
元素
简单来说 attrs
属性会包含以下内容:
- 1、大部分使用
v-bind
(或其缩写:
) 指令绑定的属性会被添加到attrs
数组中。
为什么说大部分而不是全部呢?因为在 Vue
中有个 Must Use Prop
的概念,对于一个属性如果它是 Must Use Prop
的,则该属性不会被添加到 attrs
数组中,而是会被添加到元素描述对象的 props
数组中。
如下 html
模板所示:
<div :some-attr="val"></div>
最终 attrs
数组将为:
ast = {
attrs: [
{
name: 'some-attr',
value: 'val'
}
]
}
- 2、普通的非绑定属性会被添加到
attrs
数组中。
如下 html
模板所示:
<div no-binding-attr="val"></div>
最终 attrs
数组将为:
ast = {
attrs: [
{
name: 'no-binding-attr',
value: '"val"'
}
]
}
大家观察绑定属性和非绑定属性在 attrs
数组中的却别?很容易能够发现,非绑定属性的属性值是经过 JSON.stringify
的,我们已经不止一次的提到过这么做的目的。
- 3、
slot
特性会被添加到attrs
数组中。
如下 html
模板所示:
<div slot="header"></div>
最终 attrs
数组将为:
ast = {
attrs: [
{
name: 'slot',
value: '"header"'
}
]
}
当然了由于 slot
本身是可绑定的属性,所以如果 html
模板如下:
<div :slot="header"></div>
最终 attrs
数组将为:
ast = {
attrs: [
{
name: 'slot',
value: 'header'
}
]
}
区别在于 value
值是非 JSON.stringify
化的。
实际上,并不是出现在 attrs
数组中的属性就一定会使用 setAttribute
函数将其添加到 DOM
上,例如在运行时阶段,组件会根据该组件自身的 props
定义,从 attrs
中抽离出那些作为组件 props
的属性元素。
# props
节点元素描述对象的 props
属性也是一个数组,它的格式与 attrs
数组类似。就像 attrs
数组中的属性在运行时阶段会使用 setAttribute
函数将其添加到 DOM
上一样,props
数组中的属性则会直接通过 DOM
元素对象访问并添加,举个例子,假设 props
数组如下:
ast = {
props: [
{
name: 'innerHTML',
value: '"some text"'
}
]
}
则在运行时阶段,会使用如下代码操作 DOM
:
elm.innerHTML = 'some text'
其中 elm
为 DOM
节点对象。
那么那些属性会被当做 props
呢?有两种,第一种是在绑定属性时使用了 prop
修饰符,例如:
<div :some.prop="aaa"></div>
由于绑定 some
属性的时候使用了 prop
修饰符,所以 some
属性不会出现在元素描述对象的 attrs
数组中,而是会出现在元素描述对象的 props
数组中。
第二种是那些比较特殊的属性,在绑定这些属性时,即使没有指定 prop
修饰符,但是由于它属于 Must Use Prop
的,所以这些属性会被强制添加到元素描述对象的 props
数组中,只有那些属性是 Must Use Prop
,可以查看附录:mustuseprop
# pre
节点元素描述对象的 pre
属性是一个布尔值,它的真假代表着标签是否使用了 v-pre
指令,既然是标签,所以只有当节点的类型为 1
的时候其元素描述对象才会拥有 pre
属性。
- 示例:
ast = {
type: 1,
pre: true
}
# ns
标签的 Namespace
,如果一个标签是 SVG
标签,则该标签的元素描述对象将会拥有 ns
属性,其值为 'svg'
,如果一个标签是 <math>
标签,则该标签元素描述对象的 ns
属性值为字符串 'math'
。
- 示例:
ast = {
type: 1,
ns: 'svg'
}
# forbidden
节点元素描述对象的 forbidden
属性是一个布尔值,其真假代表着该节点是否是在 Vue
模板中禁止被使用的。在 Vue
模板中满足以下条件的标签为禁止使用的标签:
- 1、
<style>
标签禁止出现在模板中。 - 2、没有指定
type
属性的<script>
标签,或type
属性值为'text/javascript'
的<script>
标签。
# parent
节点元素描述对象的 parent
属性是父节点元素描述对象的引用。
# children
节点元素描述对象的 children
属性是一个数组,存储着该节点所有子节点的元素描述对象。当然了有些节点是不可能拥有子节点的,比如普通文本节点,对于不可能拥有子节点的节点,其元素描述对象没有 children
属性。
- 示例:
ast = {
children: [
{
type: 1,
// 其他节点属性...
}
]
}
# ifConditions
如果一个标签使用 v-if
指令,则该标签的元素描述对象将会拥有 ifConditions
属性,它是一个数组。如果一个标签使用 v-else-if
或 v-else
指令,则该标签不会被添加到其父节点元素描述对象的 children
数组中,而是会被添加到相符的带有 v-if
指令节点的元素描述对象的 ifConditions
数组中。
假设有如下模板:
<div v-if="a"></div>
<h1 v-else-if="b"></h1>
<p v-else></p>
则 <div>
标签元素描述对象将是:
ast = {
type: 1,
tag: 'div',
ifConditions: [
{
exp: 'a',
block: { type: 1, tag: 'div', ifConditions: [...] /* 省略其他属性 */ }
},
{
exp: 'b',
block: { type: 1, tag: 'h1' /* 省略其他属性 */ }
},
{
exp: undefined,
block: { type: 1, tag: 'p' /* 省略其他属性 */ }
}
],
// 其他属性...
}
可以发现一个节点元素描述对象的 ifConditions
数组中也会包含节点自身的元素描述对象。
# slotName
只有 <slot>
标签的元素描述对象才会拥有 slotName
属性,代表该插槽的名字,假设模板如下:
<slot name="header" />
则元素描述对象为:
ast = {
type: 1,
tag: 'slot',
slotName: '"header"'
}
注意 <slot>
标签的 name
属性可以是绑定的:
<slot :name="dynamicName" />
则元素描述对象为:
ast = {
type: 1,
tag: 'slot',
slotName: 'dynamicName'
}
如果没有为 <slot>
标签指定 name
属性,则其元素描述对象的 slotName
属性为:
ast = {
type: 1,
tag: 'slot',
slotName: '""'
}
# slotTarget
如果一个标签使用了 slot
特性,则说明该标签将会被作为插槽的内容,为了标识该标签将被插入的位置,该标签的元素描述对象会拥有 slotTarget
属性,假如有如下模板:
<div slot="header" ></div>
则其元素描述对象为:
ast = {
type: 1,
tag: 'div',
slotTarget: '"header"'
}
我们来对比一下使用 name
属性的 <slot>
标签的元素描述对象:
ast = {
type: 1,
tag: 'slot',
slotName: '"header"'
}
可以发现 slotTarget
和 slotName
是一一对象的,这将会在运行时阶段用来寻找合适的插槽内容。
另外 slot
特性也可以是绑定的:
<div :slot="dynamicTarger" ></div>
则其元素描述对象为:
ast = {
type: 1,
tag: 'div',
slotTarget: 'dynamicTarger'
}
如果没有为 slot
特性指定属性值,则该标签元素描述对象的 slotTarget
属性的值为:
ast = {
type: 1,
tag: 'div',
slotTarget: '"default"'
}
# slotScope
我们可以使用 slot-scope
特性来指定一个插槽内容是作用域插槽,此时该标签的元素描述对象将拥有 slotScope
属性,假如有如下模板:
<div slot-scope="scopeData"></div>
其元素描述对象为:
ast = {
type: 1,
tag: 'div',
slotScope: 'scopeData'
}
# scopedSlots
同常情况下我们插槽是作为一个组件的子节点去书写的,如下:
<comp>
<div slot="header"></div>
</comp>
如上代码所示我们有自定义组件 <copm>
,并为该自定义组件提供了插槽内容。普通插槽会出现在组件元素描述对象的 children
数组中,如下是以上模板的 AST
:
ast = {
type: '1',
tag: 'comp',
children: [
{
type: 1,
tag: 'div',
slotTarget: '"header"'
}
]
}
但如果一个插槽不是普通插槽,而是作用域插槽,则该插槽节点的元素描述对象不会作为组件的 children
属性存在,而是会被添加到组件元素描述对象的 scopedSlots
属性中,假设有如下模板:
<comp>
<div slot="header" slot-scope="scopeData"></div>
</comp>
则其生成的 AST
为:
ast = {
type: '1',
tag: 'comp',
children: [],
scopedSlots: {
'"header"': {
type: 1,
tag: 'div',
slotTarget: '"header"'
}
}
}
可以发现 scopedSlots
对象的键值是作用域插槽元素描述对象的 slotTarget
属性的值。
# for、alias、iterator1、iterator2
当标签使用了 v-for
指令时,其元素描述对象将会拥有以上这四个属性,在如上四个属性中,其中 for
、alias
这两个属性是肯定存在的,而 iterator1
和 iterator2
这两个属性不一定会存在。
如果模板如下:
<div v-for="obj of list"></div>
则其元素描述对象为:
ast = {
for: 'list',
alias: 'obj'
}
如果模板如下:
<div v-for="(obj, index) of list"></div>
则其元素描述对象为:
ast = {
for: 'list',
alias: 'obj',
iterator1: 'index'
}
如果模板如下:
<div v-for="(obj, key, index) of list"></div>
则其元素描述对象为:
ast = {
for: 'list',
alias: 'obj',
iterator1: 'key'
iterator2: 'index'
}
# if、elseif、else
如果一个标签使用了 v-if
指令,则该标签元素描述对象就会拥有 if
属性,假如有如下模板:
<div v-if="a"></div>
则其元素描述对象为:
ast = {
if: 'a'
}
如果一个标签使用了 v-else-if
指令,则该标签元素描述对象就会拥有 elseif
属性,假如有如下模板:
<div v-else-if="b"></div>
则其元素描述对象为:
ast = {
elseif: 'b'
}
如果一个标签使用了 v-else
指令,则该标签元素描述对象就会拥有 else
属性,假如有如下模板:
<div v-else></div>
则其元素描述对象为:
ast = {
else: true
}
# once
使用标签使用了 v-once
指令,则该标签的元素描述对象就会包含 once
属性,它是一个布尔值,如下:
ast = {
once: true
}
# key
如果标签使用 key
特性,则该标签的元素描述对象就会包含 key
属性,假设有如下模板:
<div key="unique"></div>
则其元素描述对象为:
ast = {
key: '"unique"'
}
key
特性可以是绑定的:
<div :key="unique"></div>
则其元素描述对象为:
ast = {
key: 'unique'
}
# ref
与 key
类似,假设有如下模板:
<div ref="domRef"></div>
则其元素描述对象为:
ast = {
ref: '"domRef"'
}
ref
特性可以是绑定的:
<div :ref="domRef"></div>
则其元素描述对象为:
ast = {
ref: 'domRef'
}
# refInFor
元素描述对象的 refInFor
是一个布尔值。如果一个使用了 ref
特性的标签是使用了 v-for
指令标签的子代节点,则该标签元素描述对象的 checkInFor
属性将会为 true
,否则为 false
# component
如果标签使用 is
特性,则其元素描述对象将会拥有 component
属性,假设有如下模板:
<component :is="currentView"></component>
则其元素描述对象为:
ast = {
type: 1,
tag: 'component',
component: 'currentView'
}
is
特性也可以是非绑定的:
<table></table>
<tr is="my-row"></tr>
</table>
则 <tr>
标签的元素描述对象为:
ast = {
type: 1,
tag: 'tr',
component: '"my-row"'
}
# inlineTemplate
节点元素描述对象的 inlineTemplate
属性是一个布尔值,标识着一个组件使用使用内联模板,假设我们有如下模板:
<copm inline-template></copm>
则其元素描述对象为:
ast = {
inlineTemplate: true
}
# hasBindings
节点元素描述对象的 hasBindings
属性是一个布尔值,用来标签当前节点是否拥有绑定,所谓绑定指的就是指令。所以如果一个标签使用了指令(包括自定义指令),则其元素描述对象的 hasBindings
属性就会为 true
。
这里要强调一点,事件本身也是指令(v-on
指令),绑定的属性也是指令(v-bind
指令)。
# events、nativeEvents
如果标签使用了 v-on
指令(或缩写 @
)绑定了事件,则该标签元素描述对象中将包含 events
属性,假如有如下模板:
<div @click="handleClick"></div>
则其元素描述对象为:
ast = {
events: {
'click': {
value: 'handleClick'
}
}
}
如果在绑定事件的时候使用了修饰符,如下模板所示:
<div @click.stop="handleClick"></div>
则其元素描述对象为:
ast = {
events: {
'click': {
value: 'handleClick',
modifiers: {
stop: true
}
}
}
}
可以看到多出了 modifiers
对象。
但并不是所有修饰符都会出现在 modifiers
对象中,如下模板所示:
<div @click.once="handleClick"></div>
如上模板中我们使用了 once
修饰符,但它并不会出现在 modifiers
对象中,其最终生成的元素描述对象如下:
ast = {
events: {
'~click': {
value: 'handleClick',
modifiers: {}
}
}
}
可以看到 modifiers
是一个空对象,但是事件名字由 click
变成了 ~click
。实际上对于一个使用了 once
修饰符的事件绑定,解析器会在原始事件名称前添加 ~
符并将其作为新的事件名称,接着会忽略 once
修饰符,所以 once
修饰符不会出现在 modifiers
对象中。为什么要忽略 once
修饰符呢?因为对于后面的程序来讲,该修饰符已经没有使用的必要的,因为通过检查事件名称的第一个字符是否为 ~
即可判断该事件是否为 once
的。除了 once
修饰符之外,以下列出的修饰符也不会出现在 modifiers
对象中:
- 1、事件名称为
click
并使用了right
修饰符,则right
修饰符不会出现在modifiers
对象中,因为在解析阶段使用了right
修饰符的click
事件会被重写为contextmenu
事件,假如有如下模板:
<div @click.right="handler"></div>
其元素描述对象为:
ast = {
events: {
contextmenu: {
value: "handler",
modifiers: {}
}
}
}
- 2、
capture
、passive
修饰符不会出现在modifiers
对象中,原因与once
修饰符一样,capture
、passive
修饰符也会修改事件的名称,其中capture
修饰符会在原始事件名称之前添加!
,passive
修饰符会在事件名称之前添加&
,假如有如下模板
<div @click.capture="handler"></div>
<div @click.passive="handler"></div>
则对于的元素描述对象分别为:
// 使用了 `capture` 修饰符
ast = {
events: {
'!click': {
value: "handler",
modifiers: {}
}
}
}
// 使用了 `passive` 修饰符
ast = {
events: {
'&click': {
value: "handler",
modifiers: {}
}
}
}
- 3、
native
修饰符也不会出现在modifiers
对象中,原因很简单,native
修饰符是用来给解析器使用的,当解析器遇到使用了native
修饰符的事件,则会将事件信息添加到元素描述对象的nativeEvents
属性中,而不是events
属性中,例如:
<comp @click.native="handler"></copm>
则其元素描述对象为:
ast = {
nativeEvents: {
click: {
value: "handler",
modifiers: {}
}
}
}
除了以上修饰符之外,其他所有修饰符都会出现在 modifiers
对象中。
# directives
节点元素对象的 directives
属性是一个数组,用来保存标签中所有指令信息。但并不是所有指令信息都会保存在 directives
数组中,比如 v-for
指令和 v-if
指令等等,因为这些指令在之前的处理中已经被移除掉。总的来说,指令分为内置指令和自定义指令,真正会出现在 directives
数组中的只有部分内置指令以及全部自定义指令。
不会出现在 directives
数组中的内置指令有:v-pre
、v-for
、v-if
、v-else-if
、v-else
以及 v-once
。
会出现在 directives
数组中的内置有:v-text
、v-html
、v-show
、v-model
以及 v-cloak
。
另外 v-on
、v-bind
是两个比较特殊的指令,当这两个指令拥有参数时,则不会出现在 directives
数组中,比如:
<div v-on:click="handler"></div>
<div v-bind:some-prop="val"></div>
以上这两中写法,由于 v-on
和 v-bind
指令拥有参数,所以这两个指令不会出现在 directives
,但是我们知道 v-on
和 v-bind
指令可以直接绑定对象,此时他们是没有参数的:
<div v-on="$listeners"></div>
<div v-bind="$attrs"></div>
这时候 v-on
和 v-bind
指令都会出现在 directives
数组中。为什么同样指令不同的使用方式会得到不同的对待呢?其实正是由于使用方式的不同,才需要不同的处理,在代码生成阶段,我们会更加理解这一点。
一个完整的指令由四部分组成,分别是:指令的名称
、指令表达式
、指令参数
以及 指令修饰符
,假设有如下模板:
<div v-custom-dir:arg.modif="val"></div>
如上模板展示了一个完整的指令,最终其生成的元素描述对象为:
ast = {
directives: [
{
name: 'custom-dir',
rawName: 'v-custom-dir:arg.modif',
value: 'val',
arg: 'arg',
modifiers: {
modif: true
}
}
]
}
# staticClass
如果以标签使用了静态 class
,即非绑定的 class
,那么该标签的元素描述对象将拥有 staticClass
属性,假设有如下模板:
<div class="a b c"></div>
则其元素描述对象为:
ast = {
staticClass: '"a b c"'
}
# classBinding
staticClass
属性中存储的是静态 class
,而元素描述对象的 classBinding
属性中所存储的则是绑定的 class
,假设有如下模板:
<div :class="{ active: true }"></div>
则其元素描述对象为:
ast = {
classBinding: '{ active: true }'
}
# staticStyle、styleBinding
节点元素描述对象的 staticStyle
属于包含的是静态 style
内联样式信息,假设有如下模板:
<div style="color: red; background: green;"></div>
则其元素描述对象为:
ast = {
staticStyle: '{"color":"red","background":"green"}'
}
可以发现 staticStyle
属性的值不是简单的把 style
内联样式拷贝下来,而是将其解析成了对象的样子。
styleBinding
属性类似于 classBinding
属性。假设有如下模板:
<div :style="{ backgroundColor: green }"></div>
则其元素描述对象为:
ast = {
styleBinding: '{ backgroundColor: green }'
}
# plain
节点元素描述对象的 plain
属性是一个布尔值,plain
属性的真假将影响代码生成阶段对于 VNodeData
的生成。什么是 VNodeData
呢?在 Vue
中一个 VNode
代表一个虚拟节点,而 VNodeData
就是用来描述该虚拟节点的管家信息。在代码生成节点我们会发现 AST
中元素的大部分信息都用来生成 VNodeData
。对于一个节点的元素描述对象来讲,如果其 plain
属性值为 true
,该节点所对应的虚拟节点将不包含任何 VNodeData
。
1、如果一个标签是使用了
v-pre
指令标签的子代标签,则该标签元素描述对象的plain
属性将使用为true
。但要注意的是,使用了v-pre
指令的那个标签的元素描述对象的plain
属性不为true
。2、如果你标签既没有使用特性
key
,又没有任何属性,那么该标签的元素描述对象的plain
属性将始终为true
。
其实,我们完全可以认为,只有使用了 v-pre
指令的标签的子待节点其元素描述对象的 plain
属性才会为 true
。
# isComment
节点元素描述对象的 isComment
属性是一个布尔值,用来标识当前节点是否是注释节点。所以只有注释节点的元素描述对象才会有这个属性,并且其值为 true
。
← 编译器选项