wengjq/Blog

Vue 模板 AST

wengjq opened this issue · 0 comments

Vue 模板 AST 详解

type

节点元素描述对象的 type 属性用来标识一个节点的类型。

  • 示例:
ast = {
  type: 1
}

它有三个可取值,分别是 123,分别代表的含义是:

  • 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'

其中 elmDOM 节点对象。

那么那些属性会被当做 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-ifv-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"'
}

可以发现 slotTargetslotName 是一一对象的,这将会在运行时阶段用来寻找合适的插槽内容。

另外 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 指令时,其元素描述对象将会拥有以上这四个属性,在如上四个属性中,其中 foralias 这两个属性是肯定存在的,而 iterator1iterator2 这两个属性不一定会存在。

如果模板如下:

<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、capturepassive 修饰符不会出现在 modifiers 对象中,原因与 once 修饰符一样,capturepassive 修饰符也会修改事件的名称,其中 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-prev-forv-ifv-else-ifv-else 以及 v-once

会出现在 directives 数组中的内置有:v-textv-htmlv-showv-model 以及 v-cloak

另外 v-onv-bind 是两个比较特殊的指令,当这两个指令拥有参数时,则不会出现在 directives 数组中,比如:

<div v-on:click="handler"></div>
<div v-bind:some-prop="val"></div>

以上这两中写法,由于 v-onv-bind 指令拥有参数,所以这两个指令不会出现在 directives,但是我们知道 v-onv-bind 指令可以直接绑定对象,此时他们是没有参数的:

<div v-on="$listeners"></div>
<div v-bind="$attrs"></div>

这时候 v-onv-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