vue源码分析(十二) 编译之解析(parse)——处理标签

10/12/2018 vue源码分析面试

# 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)
      }
    }
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
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)
1

​ 这句代码的作用是获取 标签的命名空间currentParent 为当前元素的父级元素描述对象,如果当前元素存在父级并且父级元素存在命名空间,则使用父级的命名空间作为当前元素的命名空间。 如果父级元素不存在或父级元素没有命名空间,那么会通过调用 platformGetTagNamespace(tag) 函数获取当前元素的命名空间。

注意:platformGetTagNamespace 函数只会获取 svgmath 这两个标签的命名空间,但这两个标签的所有子标签都会继承它们两个的命名空间。对于其他标签则不存在命名空间。

​ 继续往下看,接下来执行的如下代码:

// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
  attrs = guardIESVGBug(attrs)
}
1
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)
1
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>
1

​ 被渲染为:

<svg xmlns:NS1="" NS1:xmlns:feature="http://www.openplans.org/topp"></svg>
1

​ 标签中多了 '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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

​ 在 guardIESVGBug 函数之前定义了两个正则常量,其中 ieNSBug 正则用来匹配那些以字符串 xmlns:NS 再加一个或多个数字组成的字符串开头的属性名,如:

<svg xmlns:NS1=""></svg>
1

​ 如上标签的 xmlns:NS1 属性将会被 ieNSBug 正则匹配成功。另外一个正则常量是 ieNSPrefix,它用来匹配那些以字符串 NS 再加一个或多个数字以及字符 : 所组成的字符串开头的属性名,如:

<svg NS1:xmlns:feature="http://www.openplans.org/topp"></svg>
1

​ 如上标签的 NS1:xmlns:feature 属性将被 ieNSPrefix 正则匹配成功。

guardIESVGBug 函数接收元素的属性数组作为参数,并返回一个新的数组,新数组与原数组结构相同。可以看到 guardIESVGBug 函数内部通过 for 循环遍历了元素的属性数组,接着使用正则 ieNSBug 去匹配属性名字,可以发现只要不满足 ieNSBug 正则的属性名,都会尝试使用 ieNSPrefix 正则去匹配该属性名并将匹配到的字符替换为空字符串。如下是渲染产生 bug 后的代码:

<svg xmlns:NS1="" NS1:xmlns:feature="http://www.openplans.org/topp"></svg>
1

​ 在解析如上标签时,传递给 start 钩子函数的标签属性数组 attrs 为:

attrs = [
  {
    name: 'xmlns:NS1',
    value: ''
  },
  {
    name: 'NS1:xmlns:feature',
    value: 'http://www.openplans.org/topp'
  }
]
1
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'
  }
]
1
2
3
4
5
6

​ 以上就是 guardIESVGBug 函数的作用。

​ 我们继续往下分析,接下来执行的代码是:

let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
  element.ns = ns
}
1
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
        }
      )
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

​ 首先判断在开发环境下执行如上代码,outputSourceRange 的作用是判断生产环境还是开发环境,开发环境为 true

​ 所以当outputSourceRangetrue 时,给element 添加startend属性,分别将参数 startend赋值给对应的属性。

​ 我们在上一章分析 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":[]
}
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
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}
}
1
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 }
  )
}
1
2
3
4
5
6
7
8
9
10
11

​ 首先我们先来看一下 isForbiddenTagisServerRendering 的定义,如下:

​ 源码目录: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'
    ))
  )
}
1
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
}
1
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
}
1
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)
}
1
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)
  }
}
1
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']
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

checkRootConstraints 作用是用来检测模板根元素是否符合要,不能使用 slottemplate 标签作为模板根元素,这是因为 slot 作为插槽,它的内容是由外界决定的,而插槽的内容很有可能渲染多个节点,template 元素的内容虽然不是由外界决定的,但它本身作为抽象组件是不会渲染任何内容到页面的,而其又可能包含多个子节点,所以也不允许使用 template 标签作为根节点。总之这些限制都是出于 必须有且仅有一个根元素 考虑的。

​ 所有这段代码的作用是判断 root 是否存在,如果 root 不存在那说明当前元素应该就是根元素,所以在 if 语句块内直接将当前元素的描述对象 element 赋值给 root 变量,同时在开发环境中会调用上面刚刚讲过的 checkRootConstraints 函数检查根元素是否符合要求。

​ 我们继续往下分析,接下来执行的是如下代码:

if (!unary) {
  currentParent = element
  // 为parse函数,stack标签堆栈添加一个标签
  stack.push(element)
} else {
  // 关闭节点
  closeElement(element)
}
1
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-ifv-else-if 以及 v-else 保证有且仅有一个根元素被渲染。
  • 8、构建 AST 并建立父子级关系是在 start 钩子函数中完成的,每当遇到非一元标签,会把它存到 currentParent 变量中,当解析该标签的子节点时通过访问 currentParent 变量获取父级元素。
  • 9、如果一个元素使用了 v-else-ifv-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
}
1
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
  }
}
1
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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

​ 首先 getAndRemoveAttr 函数接收三个参数分别为:

  • elAST
  • 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":[]
}
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
28
29

​ 通过上面的例子我们知道,el.attrsMapel.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 }
 ]
1
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 }
 ]
1
2
3
4

​ 接下来通过 if (removeFromMap) 判断第三个参数是否为真,如果为真则执行 if 语句,否则跳过 if 语句,在我们当前的案例中,removeFromMap = true 所以执行 delete el.attrsMap[name],删除 el.attrsMap 中的 v-show 属性,此时 attrsMap 变为如下值:

"attrsMap":{
  ":class":"classObject",
  "class":"list"
}
1
2
3
4

​ 最后返回 val ,如果 val 不存在则返回 undefined ,在此案例中返回 isShow

​ 到此为止,我们终于知道了 processPre 的作用是获取给定元素 v-pre 属性的值,如果 v-pre 属性的值不等于 null 则会在元素描述对象上添加 .pre 属性,并将其值设置为 true

​ 我们再回到 start 函数,执行完 ,继续执行下列语句:

if (element.pre) { 
  inVPre = true 
}
1
2
3

​ 此段代码判断了元素对象的 .pre 属性是否为真,我们知道假如一个标签使用了 v-pre 指令,那么经过 processPre 函数处理之后,该元素描述对象的 .pre 属性值为 true,这时会将 inVPre 变量的值也设置为 true。当 inVPre 变量为真时,意味着 后续的所有解析工作都处于 v-pre 环境下,编译器会跳过拥有 v-pre 指令元素以及其子元素的编译过程,所以后续的编译逻辑需要 inVPre 变量作为标识才行。

​ 我们继续往下分析,接下来执行如下代码:

if (platformIsPreTag(element.tag)) {
  inPre = true
}
1
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) {
  /* 省略 */
}
1
2
3
4
5

​ 在分析 processRawAttrs 之前,我们还是已下面的案例来分析,如下:

<div v-pre v-on:click="handleClick"></div>
1

​ 此段代码转换为 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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

​ 通过上一小节的分析,此处 div 标签拥有 v-pre 指令,所以 inVPretrue ,此时会执行 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
  }
}
1
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
}
1
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
1
2

​ 获取 attrsList 属性和attrsList 数组的长度,在此案例中attrsList.length的值为 1, attrsList值为:

"attrsList":[
  {"name": "v-on:click", "value": "handleClick", "start": 11, "end": 35}
]
1
2
3

​ 接下来通过 if (len) 判读 attrsList.length 的值,此处为1,所以执行 if 语句,接着通过 const attrs: Array<ASTAttr> = el.attrs = new Array(len) 创建一个长度等于 attrsList.length 的数组并给 el 添加 attrs 值为新创建的数组。然后通过 for 循环给 attrs 添加 namevaluestartend属性。

​ 注意这里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>
1
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>
1
2
3

​ 我们看一下 处理 v-for 的代码,如下:

​ 源码目录:src/compiler/parser/index.js

if (inVPre) {
  /* 省略 */
} else if (!element.processed) {
  // structural directives
  processFor(element)
  /* 省略 */
}
1
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']
      )
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

processFor 中首先通过 getAndRemoveAttr 移除 element.attrsList 对象中 namev-for 的属性,并且返回获取到 v-for 属性的值赋值给变量 exp,例如 v-for="(l, i) in list" 获取到的 exp(l, i) in list

​ 接下来通过 parseForv-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
}
1
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"]
1

说明:关于 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)
1

​ 这里定义了 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()
}
1
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"
}
1
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>
1
2
3

​ 此时我们获取到的 iteratorMatch 值为 [',k ,i', 'k', 'i'] ,所以 iteratorMatch[2]i ,此时 res 为,如下对象:

res = {
  alias: "l"
  for: "list",
  iterator1: "k",
  iterator2: "i"
}
1
2
3
4
5
6

​ 最后当为空时,执行 else 语句,如下:

if (iteratorMatch) {
  /* 省略 */
} else {
  res.alias = alias
}
1
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>
1
2
3

​ 此时执行 else 语句,所以最终 res 为,如下对象:

res = {
  alias: "l"
  for: "list"
}
1
2
3
4

说明:关于 forIteratorRE 我们已经在前面章节中做了详细分析,请移步到 这里 (opens new window) 学习。

​ 至此, parseFor 我们已经分析完了。

​ 我们继续回到 processFor ,在此案例中,通过 const res = parseFor(exp) 解析完 v-for 得到的结果如下:

const res = {
  alias: "l",
  for: "list",
  iterator1: "i"
}
1
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']
  )
}
1
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)
  /* 省略 */
}
1
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
    }
  }
}
1
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>
1
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
}
1
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')
1

getAndRemoveAttr 我们前面已经分析过了,在这里的作用是移除 namev-if 的属性,并且返回获取到 v-if 属性的值,例如 v-if="child === 1" 获取到的 expchild === 1。在我们这个案例中 exp 有值,继续执行 if 语句,如下:

if (exp) {
  el.if = exp
  addIfCondition(el, {
    exp: exp,
    block: el
  })
} else {
  /* 省略*/
}
1
2
3
4
5
6
7
8
9

​ 首先在元素描述对象上定义了 el.if 属性,并且该属性的值就是 v-if 指令的属性值即 el.if = 'child === 1'。接下来调用 addIfCondition 函数,第一个参数就是当前元素描述对象本身;第二个参数是一个对象,包含两个属性 expblock 值分别为 expel,我们再来看看addIfCondition的定义,如下:

export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
  if (!el.ifConditions) {
    el.ifConditions = []
  }
  el.ifConditions.push(condition)
}
1
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
  }
}
1
2
3
4
5
6
7
8
9
10
11

else 的处理逻辑和处理 v-if 时类似,这里主要是对 v-elsev-else-if 做处理,即:

  • 移除 namev-else 的属性,并且返回空字符串即 '',并在元素描述对象上定义了 el.else 属性,并且该属性的值就是 true
  • 移除 namev-else-if 的属性,并且返回获取到 v-else-if 属性的值,例如 v-else-if="child === 2" 获取到的 elseifchild === 2,然后在元素描述对象上定义了 el.elseif 属性,并且该属性的值就是 v-elseif 指令的属性值

注意:

​ 对于使用了 v-elsev-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)
}
1
2
3
4
5
6

​ 在分析 processOnce 之前我们还是用一个案例来说明,代码如下:

<div>
  <div v-once> v-once </div>
</div>
1
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
}
1
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;
  }
}
1
2
3
4
5
6

processOnce 的作用是移除 namev-once 的属性,并且返回空字符串即 '',并在元素描述对象上定义了 el.once 属性,并且该属性的值就是 true

# 2.7 closeElement

​ 接下来我们分析一下 closeElement 函数,首先看一下如下代码:

​ 源码目录:src/compiler/parser/index.js

function closeElement (element) {
  trimEndingWhitespace(element)
  /* 省略... */
}
1
2
3
4

​ 在分析 closeElement 具体的执行逻辑之前,我们函数以一个案例进行分析,如下:

<div>
  <input type="text" v-model="val">
</div>
1
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
}
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()
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

​ 此案例中 inPrefalse ,所以继续执行 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 中声明的符号节点
1
2
3
4
5
6
7
8
9
10
11
12

​ 接下来执行,如下代码:

if (!inVPre && !element.processed) {
  element = processElement(element, options)
}
1
2
3

​ 此时 input 标签的处理 inVPrefalseprocessed 表示是否已经处理过,此时也是 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 }
    )
  }
}
1
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-ifv-else-ifv-else

​ 在分析 processIf 时我们知道如果发现元素的属性中有 v-ifv-else-ifv-else,会在元素描述对象上添加相应的属性作为标识即 .if 属性、.elseif 属性以及 .else 属性。

​ 首先 root.if 必须为真,要知道一点,即无论定义多少个根元素,root 变量始终存储的是第一个根元素的描述对象,所以 root.if 为真就保证了第一个定义的根元素是使用了 v-if 指令的。同时条件 (element.elseif || element.else) 也必须为真,注意这里是 element.elseifelement.else,而不是 root.elseifroot.elseroot 为第一个根元素的描述对象,element 为当前元素描述对象,即非第一个根元素的描述对象。如果以上条件成立就能够保证所有根元素都是由 v-ifv-else-ifv-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>
1
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' /* 省略其他属性 */ }
    }
  ]
  // 省略其他属性...
}
1
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
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

​ 当前元素存在父级(currentParent),并且当前元素不是被禁止的元素。只有在这种情况下才会执行该 if 条件语句块内的代码。紧接着又是一个 if 语句判断 element.elseif || element.else,如果当前元素使用了 v-else-ifv-else 指令,则会调用 processIfConditions 函数。

​ 如果当前元素没有使用了 v-else-ifv-else 指令,则执行 else 语句。else 语句中又是一个条件判断 element.slotScope 即当前元素是否使用了 slot-scope 特性。

​ 如果一个元素使用了 slot-scope 特性,那么该元素的描述对象会被添加到父级元素的 scopedSlots 对象下,也就是说使用了 slot-scope 特性的元素与使用了 v-else-ifv-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)
1
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)
}
1
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']
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

​ 首先通过 findPrevElement 函数找到当前元素的前一个元素描述对象,并将其赋值给 prev 常量,接着进入 if 条件语句,判断当前元素的前一个元素是否使用了 v-if 指令,我们知道对于使用了 v-else-ifv-else 指令的元素来讲,他们的前一个元素必然需要使用相符的 v-if 指令才行。如果前一个元素确实使用了 v-if 指令,那么则会调用 addIfCondition 函数将当前元素描述对象添加到前一个元素的 ifConditions 数组中。如果前一个元素没有使用 v-if 指令,那么此时将会进入 else...if 条件语句的判断,即如果是非生产环境下,会打印警告信息提示开发者没有相符的使用了 v-if 指令的元素。

​ 以上是当前元素使用了 v-else-ifv-else 指令时的特殊处理,由此可知 当一个元素使用了 v-else-ifv-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()
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

​ 首先 findPrevElement 函数只用在了 processIfConditions 函数中,它的作用就是当解析器遇到一个带有 v-else-ifv-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>
1
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
  }
}
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
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>
1
2
3

processKey 首先调用 getBindingAttrAST 树进行处理,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)
    }
  }
}
1
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 语句,它是通过调用 parseFiltersdynamicValue 为参数,那么 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}`
  }
}
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
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.tagtemplate 则报警告,或者el.tagtransition-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>
1
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)
  }
}
1
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
}
1
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>
1
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>
1
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>
1
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>
1
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>
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
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>
1
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
  }
  /* 省略 */
}
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
28
29
30
31

​ 首先 if 语句判断 el.tag === 'template' 标签是 template ,通过 getAndRemoveAttr 移除 element.attrsList 对象中 namescope 的属性,并且返回获取到 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 对象中 nameslot-scope 的属性,并且返回获取到 slot-scope 属性的值赋值给变量 slotScope,最后在元素描述对象上添加了 el.slotScope 属性。

else if 语句中,还一段逻辑作用是,在非生产环境下,会检查当前元素是否使用了 v-for 属性,如下代码所示:

<div slot-scope="slotProps" v-for="item of slotProps.list"></div>
1

​ 如上这句代码中,slot-scope 属性与 v-for 指令共存,这会造成什么影响呢?由于 v-for 具有更高的优先级,所以 v-for 绑定的状态将会是父组件作用域的状态,而不是子组件通过作用域插槽传递的状态。并且这么使用很容易让人感到困惑。更好的方式是像如下代码这样:

<template slot-scope="slotProps">
  <div v-for="item of slotProps.list"></div>
</template>
1
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'))
    }
  }
  /* 省略 */
}
1
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>
1

​ 这时通过 getBindingAttr 函数获取 slot 属性的值时,会得到字符串 "",此时会将 el.slotTarget 属性的值设置为字符串 '"default"',否则直接将 slotTarget 变量的值赋值给 el.slotTarget 属性。

​ 然后通过 el.attrsMap[':slot'] || el.attrsMap['v-bind:slot'] 获取动态绑定的 slot 的属性并通过 !! 转换为布尔类型,并在元素描述对象上添加了 el.slotTargetDynamic 属性,值为转换后的布尔值。

​ 接下来是一个 if 语句,这段代码的作用就是用来保存原生影子DOM (shadow DOM)的 slot 属性,当然啦既然是原生影子 DOMslot 属性,那么首先该元素必然应该是原生 DOM ,所以 el.tag !== 'template' 必须成立,同时对于作用域插槽是不会保留原生 slot 属性的。关于原生影子 DOMslot 属性,更详细的内容大家可以阅读 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
      }
    }
  }
}
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
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" ,对应的 attrsListattrsList: [{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
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

​ 我们回到 processSlotContent 继续往下看,接下来判断 el.slotScopeel.slotTarget 存在,说明使用了 slotslot-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>
1
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
1
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.slotTargetel.slotTargetDynamicel.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>
1
2
3
4
5
6
7

v-slot:default="todo" ,对应的 attrsListattrsList: [{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.slotScopeel.slotTarget 存在,说明使用了 slotslot-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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

​ 这段代码的主要作用是:

  • 获取当前组件的 scopedSlots
  • 获取到 name, dynamic
  • 获取 slotskey 对应匹配出来 nameslot,然后再其下面创建一个标签名为 templateASTElementattrs 为空数组,parent 为当前节点
  • name、dynamic 统一赋值给 slotContainerslotTarget、slotTargetDynamic,而不是 el
  • 将当前节点的 children 添加到 slotContainerchildren 属性中
  • 清空当前节点的 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 }
}
1
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 首先通过正则表达式 slotREv-slotv-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')
      )
    }
  }
}
1
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>
1

​ 则 el.slotName 属性的值为 JSON.stringify('header')

​ 如果我们的 <slot> 标签如下:

<slot></slot>
1

​ 则 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.`
  )
}
1
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
  }
}
1
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- 开头的特性,如 keyref 等,在获取这些属性的值时,是通过 getBindingAttr 函数获取的,不过 slot-scopescopeinline-template 这三个属性虽然没有以 v- 开头,但仍然使用 getAndRemoveAttr 函数获取其属性值。但这并不是关键,关键的是我们要知道使用 getAndRemoveAttrgetBindingAttr 这两个函数获取属性值的时候到底有什么区别。

​ 我们知道类似于 v-forv-if 这类以 v- 开头的属性,在 Vue 中我们称之为指令,并且这些属性的属性值是默认情况下被当做表达式处理的,比如:

<div v-if="a && b"></div>
1

​ 如上代码在执行的时候 ab 都会被当做变量,并且 a && b 是具有完整意义的表达式,而非普通字符串。并且在解析阶段,如上 div 标签的元素描述对象的 el.attrsList 属性将是如下数组:

el.attrsList = [
  {
    name: 'v-if',
    value: 'a && b'
  }
]
1
2
3
4
5
6

​ 这时,当使用 getAndRemoveAttr 函数获取 v-if 属性值时,得到的就是字符串 'a && b',但不要忘了这个字符串最终是要运行在 new Function() 函数中的,假设是如下代码:

new Function('a && b')
1

​ 那么这句代码等价于:

function () {
  a && b
}
1
2
3

​ 可以看到,此时的 a && b 已经不再是普通字符串了,而是表达式。

​ 这就意味着 slot-scopescopeinline-template 这三个属性的值,最终也将会被作为表达式处理,而非普通字符串。如下:

<div slot-scope="slotProps"></div>
1

​ 如上代码是使用作用域插槽的典型例子,我们知道这里的 slotProps 确实是变量,而非字符串。

​ 那如果使用 getBindingAttr 函数获取 slot-scope 属性的值会产生什么效果呢?由于 slot-scope 并非 v-bind:slot-scope:slot-scope,所以在使用 getBindingAttr 函数获取 slot-scope 属性值的时候,将会得到使用 JSON.stringify 函数处理后的结果,即:

JSON.stringify('slotProps')
1

​ 这个值就是字符串 '"slotProps"',我们把这个字符串拿到 new Function() 中,如下:

new Function('"slotProps"')
1

​ 如上这句代码等价于:

function () {
  "slotProps"
}
1
2
3

可 以发现此时函数体内只有一个字符串 "slotProps",而非变量。

​ 但并不是说使用了 getBindingAttr 函数获取的属性值最终都是字符串,如果该属性是绑定的属性(使用 v-bind:),则该属性的值仍然具有 javascript 语言的能力。否则该属性的值就是一个普通的字符串。

​ 如下是前面已经处理过的属性:

  • v-pre
  • v-for
  • v-ifv-else-ifv-else
  • v-once
  • key
  • ref
  • slotslot-scopescopenamev-slot
  • isinline-template

​ 如上属性中包含了部分 Vue 内置的指令(v- 开头的属性),大家可以对照一下 Vue 的官方文档,查看其内置的指令,可以发现之前的讲解中不包含对以下指令的解析:

  • v-textv-htmlv-showv-onv-bindv-modelv-cloak

​ 除了这些指令之外,还有部分属性的处理我们也没讲到,比如 class 属性和 style 属性,这两个属性比较特殊,因为 Vue 对他们做了增强,实际上在transforms 中有对于 class 属性和 style 属性的处理,这个我们后面会统一讲解。

​ 以如上列出的属性为例,下表中总结了特定的属性与获取该属性值的方式:

属性 获取属性值的方式
v-pre getAndRemoveAttr
v-for getAndRemoveAttr
v-ifv-else-ifv-else getAndRemoveAttr
v-once getAndRemoveAttr
key getBindingAttr
ref getBindingAttr
name getBindingAttr
slot-scopescope getAndRemoveAttr
slot getBindingAttr
v-slot getAndRemoveAttrByRegex
is getBindingAttr
inline-template getAndRemoveAttr

​ 老规矩在分析之前我们还是以一个案例来分析,源码如下:

<div :class="classObject" class="list" v-show="isShow" @click="clickItem(index)"> 
  {{ val }} 
</div>
1
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":[]
}
1
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++) {
    /* 省略... */
  }
}
1
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"
}
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
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 {
      /* 省略... */
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

for 里面首先分别为 namerawName 以及 value 变量赋了值,其中 namerawName 变量中保存的是属性的名字,而 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 {
      /* 省略... */
    }
  }
}
1
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
  }
}
1
2
3
4
5
6
7
8

parseModifiers 主要做了两件事:

  • 通过 modifierRE 匹配修饰分,例如 namebind:prop1.prop 所以匹配到的结果为 ['.prop'],如果匹配失败则会得到 null
  • 遍历 match 数组,从修饰符的第一位开始截取到末尾即得到 prop ,然后以 propkey 值添加到 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 {
      /* 省略... */
    }
  }
}
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

​ 首先我们看一下 if 的条件表达式 bindRE.test(name) 其中 bindRE 用来匹配以字符 : 或字符串 v-bind: 或字符 . 开头的字符串,在们上面例子中 namev-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 匹配以字符 [ 开头并以字符 ] 结尾的字符串,作用是判断是否为动态属性,在我们当前案例中,此时的 nameprop1 所以不是动态属性,即 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 {
      /* 省略... */
    }
  }
}
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

​ 这个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 {
      /* 省略... */
    }
  }
}
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

​ 接下来又是一个 if 语句,在这里的作用主要是处理修饰符,我们先来看一下对修饰符 prop 的处理,在分析之前我们要对我们的案例中 v-bind:prop1='prop1' 做一点修改即加一个 prop 修饰符,例如:v-bind:prop1.prop='prop1'。接下来我们从前面的分析知道 modifiers = {prop: true}isDynamicfalse 所以条件为真,执行 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 {
      /* 省略... */
    }
  }
}
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

​ 这个 if 语句是对修饰符 camel 的处理,如果 modifiers.camel 为真,则说明该绑定的属性使用了 camel 修饰符,使用该修饰符的作用只有一个,那就是将绑定的属性驼峰化。例如 v-bind:prop-data='prop' ,转换后 namepropData

​ 接着我们来看一下对于最后一个修饰符的处理,即 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 {
      /* 省略... */
    }
  }
}
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
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" />
1

​ 这句代码等价于:

<template>
  <child :some-prop="value" @update:someProp="handleEvent" />
</template>

<script>
export default {
  data () {
    value: ''
  },
  methods: {
    handleEvent (val) {
      this.value = val
    }
  }
}
</script>
1
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})`
  }
}
1
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 {
      /* 省略... */
    }
  }
}
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

​ 首先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 {
      /* 省略... */
    }
  }
}
1
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) 所以获取到的 nameclick,接下来判断 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
) {
  /* 省略 */
}
1
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
    )
  }
  /* 省略 */
}
1
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'
    }
  }
  /* 省略 */
}
1
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',先是判断是否为动态属性,如果是,此时 namename = "(click)==='click'?'contextmenu':'click'" ,如果nameclick 则直接给 name 赋值 contextmenu 并且删除 right 修饰符。

​ 接着判断修饰符middle如果存在,就执行 if 语句,if 里面又是一个 if...else 语句块,我们以一个例子来说明, 例如 @click.middle='handleClick',先是判断是否为动态属性,如果是,此时 namename = "(click)==='click'?'mouseup':'click'" ,如果nameclick 则直接给 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)
  }
  /* 省略 */
}
1
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 = {})
  }
  /* 省略 */
}
1
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
  }
  /* 省略 */
}
1
2
3
4
5
6
7
8
9
10

​ 首先定义了 newHandler 对象,通过 rangeSetItem 为该对象初始 valuedynamicstartend属性,value 属性的值就是 v-on 指令的属性值,startend 分别为 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 }
1

​ 再经过 rangeSetItem 生成的 newHandler 为:

{ dynamic: false, end: 124, modifiers: {}, start: 94, value: "clickItem(index)"}
1

​ 我们再来看一下最后一段代码,源码如下:

​ 源码目录: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
}
1
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 变量都是一个空对象,所以在第一次调用 addHandlerhandlers 常量是 undefined,这就会导致接下来的代码中 else 语句块将被执行。

​ 可以看到在 else 语句块内,为 events 对象定义了与事件名称相同的属性,并以 newHandler 对象作为属性值。

​ 我们以上面的例子 @click.once="clickItem(index)" 来分析,最终生成的 newHandler 为:

// 注意这里是空对象,因为 modifiers.once 修饰符被 delete 了
{ dynamic: false, end: 124, modifiers: {}, start: 94, value: "clickItem(index)"}
1
2

​ 又因为使用了 once 修饰符,所以事件名称将变为字符串 '~click',又因为在监听事件时没有使用 native 修饰符,所以 events 变量是元素描述对象的 el.events 属性的引用,所以调用 addHandler 函数的最终结果就是在元素描述对象的 el.events 对象中添加相应事件的处理结果,如下:

el.event = {
  '~click': { dynamic: false, end: 124, modifiers: {}, start: 94, value: "clickItem(index)"}
}
1
2
3

​ 现在我们来修改一下之前的模板,如下:

<div @click.prevent="handleClick1" @click="handleClick2"></div>
1

​ 如上模板所示,我们有两个 click 事件的侦听,其中一个 click 事件使用了 prevent 修饰符,而另外一个 click 事件则没有使用修饰符,所以这两个 click 事件是不同,但这两个事件的名称却是相同的,都是 'click',所以这将导致调用两次 addHandler 函数添加两次名称相同的事件,但是由于第一次调用 addHandler 函数添加 click 事件之后元素描述对象的 el.events 对象已经存在一个 click 属性,如下:

el.events = {
  click: {
    value: 'handleClick1',
    modifiers: { prevent: true }
  }
}
1
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'
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11

这还没完,我们再次尝试修改我们的模板:

<div @click.prevent="handleClick1" @click="handleClick2" @click.self="handleClick3"></div>
1

​ 我们在上一次修改的基础上添加了第三个 click 事件侦听,但是我们使用了 self 修饰符,所以这个 click 事件与前两个 click 事件也是不同的, if 语句块的代码将被执行。

​ 由于此时 el.events.click 属性已经是一个数组,所以如上 if 语句的判断条件成立。在 if 语句块内执行了一句代码,这句代码是一个三元运算符,其作用很简单,我们知道 important 所影响的就是事件作用的顺序,所以根据 important 参数的不同,会选择使用数组的 unshift 方法将新添加的事件信息对象放到数组的头部,或者选择数组的 push 方法将新添加的事件信息对象放到数组的尾部。这样无论你有多少个同名事件的监听,都不会落下任何一个监听函数的执行。

​ 接着我们注意到 addHandler 函数的最后一句代码,如下:

el.plain = false
1

​ 如果一个标签存在事件侦听,无论如何都不会认为这个元素是“纯”的,所以这里直接将 el.plain 设置为 falseel.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 {
    /* 省略... */
  }
}
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
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-textv-htmlv-showv-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 方法,所以如果匹配成功则会返回一个结果数组,匹配失败则会得到 nullargRE 正则用来匹配指令字符串中的参数部分,并且拥有一个捕获组用来捕获参数字符串,假设现在 name 变量的值为 custom:arg,则最终 argMatch 常量将是一个数组:

const argMatch = [':arg', 'arg']
1

​ 可以看到 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}
)
1
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
}
1
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
  }
}
1
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>
1
2
3

​ 假设如上代码中的 list 数组如下:

[1, 2, 3]
1

​ 此时将会渲染三个输入框,但是当我们修改输入框的值时,这个变更是不会体现到 list 数组的,换句话说如上代码中的 v-model 指令无效,为什么无效呢?这与 v-for 指令的实现有关,如上代码中的 v-model 指令所执行的修改操作等价于修改了函数的局部变量,这当然不会影响到真正的数据。为了解决这个问题,Vue 也给了我们一个方案,那就是使用对象数组替代基本类型值的数组,并在 v-model 指令中绑定对象的属性,我们修改一下上例并使其生效:

<div v-for="obj of list">
  <input v-model="obj.item" />
</div>
1
2
3

​ 此时在定义 list 数组时,应该将其定义为:

[
  { item: 1 },
  { item: 2 },
  { item: 3 },
]
1
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])
    }
  }
}
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
28
29
30
31
32
33
34
35
36
37
这块代码的作用就是用来处理非指令属性的,如下列出的非指令属性是我们在之前的讲解中已经讲过的属性:
  • key
  • ref
  • slotslot-scopescopename
  • isinline-template

​ 这些非指令属性都已经被相应的处理函数解析过了,所以 processAttrs 函数是不负责处理如上这些非指令属性的。换句话说除了以上这些以外,其他的非指令属性基本都由 processAttrs 函数来处理,比如 idwidth 等,如下:

<div id="div1" :class="classObject" class="list" prop2="prop2"> 
  {{ val }} 
</div>
1
2
3

​ 如上 div 标签中的 id 属性和 width 属性都会被 processAttrs 函数处理,注意 class 不会被processAttrs处理,是通过前面的 transforms[i](element, options) 进行处理的

​ 首先在非生产环境下才会执行该 if 语句块内的代码,在该 if 语句块内首先调用了 parseText 函数,parseText函数的作用是用来解析字面量表达式的,什么是字面量表达式呢?如下模板代码所示:

<div id="{{ isTrue ? 'a' : 'b' }}"></div>
1

​ 其中字符串 "b" 就称为字面量表达式,此时就会使用 parseText 函数来解析这段字符串。如果使用 parseText 函数能够成功解析某个非指令属性的属性值字符串,则说明该非指令属性的属性值使用了字面量表达式,就如同上面的模板中的 id 属性一样。此时将会打印警告信息,提示开发者使用绑定属性作为替代,如下:

<div :id="isTrue ? 'a' : 'b'"></div>
1

​ 我们往下继续看代码,接下来是 执行 addAttr 函数,将该属性与该属性对应的字符串值添加到元素描述对象的 el.attrsel.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
}
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

​ 前面我们分析了 processElement 函数中的一系列 processXXX 函数,还有两段代码没有分析,首先是:

element.plain = (
  !element.key && //如果没有key 
  !element.scopedSlots && //也没有作用域插槽
  !element.attrsList.length // 也没有属性
)
1
2
3
4
5

​ 这句代码的作用是:当结构化的属性(structural attributes)被移除之后,检查该元素是否是“纯”的

​ 我们知道 v-forv-if/v-else-if/v-elsev-once 等指令会被认为是结构化的指令(structural directives)。这些指令在经过 processForprocessIf 以及 processOnce 等函数处理之后,会把这些指令从元素描述对象的 attrsList 数组中移除。

​ 这段代码判断了元素描述对象的 key 属性是否存在,并且判断元素描述对象的 scopedSlots 属性是否存在,同时检查了元素描述对象的 attrsList 数组是否为空。通过如上条件可知,只有当标签没有使用 keyscopedSlots属性,并且标签没有使用了结构化指令的情况下才被认为是“纯”的,此时会将元素描述对象的 plain 属性设置为 true。我们暂且记住这一点,当后面讲解静态优化和代码生成时我们会看到 plain 属性的作用。

​ 最后是还有一个 for 循环,如下:

for (let i = 0; i < transforms.length; i++) {
  element = transforms[i](element, options) || element
}
1
2
3

​ 使用一个 for 循环遍历了 transforms 数组,并执行数组中的 transformNode 函数。

说明:关于 transformNode 我们会在后面的章节中做详细分析。

transformNode 定义在 src/platforms/web/compiler/modules/style.js

# 2.17 preTransformNode

​ 我们在 第十一章 (opens new window) 中 已经分析过 preTransforms 最终为 :

[ preTransformNode ]
1

​ 数组中只有一个元素 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
    }
    /* 省略... */
  }
}
1
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`
    }
    /* 省略... */
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

​ 在分析之前我们还是用一个案例来具体说明,如下:

<input v-model="val" :type="inputType" />
1

​ 首先判断标签有没有通过 :v-bind: 绑定 type 属性,如果存在其一,则使用 getBindingAttr 函数获取绑定的 type 属性的值。

​ 接下来如果该 if 条件语句的判断条件成立,则说明该 input 标签没有使用非绑定的 type 属性,并且也没有使用 v-bind:: 绑定 type 属性,并且开发者使用了 v-bind,但仍然可以通过如下方式绑定属性:

<input v-model="val" v-bind="{ type: inputType }" />
1

​ 此时就需要通过读取绑定对象的 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]">
 */
1
2
3
4
5
6
7
8
9

​ 根据如上注释可知 preTransformNode 函数会将形如:

<input v-model="data[type]" :type="type">
1

​ 这样的 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]">
1
2
3

​ 我们知道在 AST 中一个标签对应一个元素描述对象,所以从结果上看,preTransformNode 函数将一个 input 元素描述对象扩展为三个 input 标签的元素描述对象。但是由于扩展后的标签由 v-ifv-else-ifv-else 三个条件指令组成,我们在前面的分析中得知,对于使用了 v-else-ifv-else 指令的标签,其元素描述对象是会被添加到那个使用 v-if 指令的元素描述对象的 el.ifConditions 数组中的。所以虽然把一个 input 标签扩展成了三个,但实际上并不会影响 AST 的结构,并且从渲染结果上看,也是一致的。

​ 但为什么要将一个 input 标签扩展为三个呢?这里有一个重要因素,由于使用了绑定的 type 属性,所以该 input 标签的类型是不确定的,我们知道同样是 input 标签,但类型为 checkboxinput 标签与类型为 radioinput 标签的行为是不一样的。到代码生成的阶段大家会看到正是因为这里将 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)
      /* 省略... */
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

​ 这段代码定义了四个常量,分别是 ifConditionifConditionExtrahasElse 以及 elseIfCondition,其中 ifCondition 常量保存的值是通过 getAndRemoveAttr 函数取得的 v-if 指令的值,注意如上代码中调用 getAndRemoveAttr 函数时传递的第三个参数为 true,所以在获取到属性值之后,会将该属性从元素描述对象的 el.attrsMap 中移除。

​ 假设我们有如下模板:

<input v-model="val" :type="inputType" v-if="display" />
1

​ 则 ifCondition 常量的值为字符串 'display'

​ 第二个常量 ifConditionExtra 同样是一个字符串,还是以如上模板为例,由于 ifCondition 常量存在,所以 ifConditionExtra 常量的值为字符串 '&&(display)',假若 ifCondition 常量不存在,则 ifConditionExtra 常量的值将是一个空字符串。

​ 第三个常量 hasElse 是一个布尔值,它代表着 input 标签是否使用了 v-else 指令。其实现方式同样是通过 getAndRemoveAttr 函数获取 v-else 指令的属性值,然后将值与 null 做比较。如果 input 标签使用 v-else 指令,则 hasElse 常量的值为真,反之为假。

​ 第四个常量 elseIfConditionifCondition 类似,只不过 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
      })
      /* 省略... */
    }
  }
}
1
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 />
1

​ 大家想象一下这样设计是否合理?我认为这是合理的,对于一个既使用了 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)
1
2
3
4

​ 实际上 preTransformNode 函数的处理逻辑就是把一个 input 标签扩展为多个标签,并且这些扩展出来的标签彼此之间是互斥的,后面大家会看到这些扩展出来的标签都存在于元素描述对象的 el.ifConditions 数组中。

​ 在 processFor 函数和 processElement 函数中调用了 addRawAttr 函数,作用就是将属性的名和值分别添加到元素描述对象的 el.attrsMap 对象以及 el.attrsList 数组中。

​ 以如下这句话为例:

addRawAttr(branch0, 'type', 'checkbox')
1

​ 这么做就等价于把新克隆出来的标签视作:

<input type="checkbox" />
1

​ 接下来把元素描述对象的 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
      })
      /* 省略... */
    }
  }
}
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

​ 这段代码的作用就是创建单选按钮和其他类型的 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
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

​ 首先判断该标签是否使用了 v-else-ifv-else指令,如果使用则给标签的元素描述对象添加 elseelseif 属性,并赋值。最后返回第一个克隆的元素描述对象。

# 2.18 transformNode

​ 理函数 transformNode 的作用是对 class 属性和 style 属性进行扩展。

​ 我们在 第十一章 (opens new window) 中 已经分析过 transforms 最终为 :

[ 
  transformNode, // class
  transformNode // style
]
1
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
  }
}
1
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>
1

​ 首先定义 warn 常量,它是一个函数,用来打印警告信息。接着使用 getAndRemoveAttr 函数从元素描述对象上获取非绑定的 class 属性的值,并将其保存在 staticClass 常量中。接着进入一段 if 条件语句,在非生产环境下,并且非绑定的 class 属性值存在,则会使用 parseText 函数解析该值,如果解析成功则说明你在非绑定的 class 属性中使用了字面量表达式,例如:

<div class="{{ isActive ? 'active' : '' }}"></div>
1

​ 这时 Vue 会打印警告信息,提示你使用如下这种方式替代:

<div :class="{ 'active': isActive }"></div>
1

​ 接下来判断如果非绑定的 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
  }
}
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

​ 用来处理 style 属性的 transformNode 函数基本与用来处理 class 属性的 transformNode 函数相同。

​ 与 class 属性不同,如果一个标签使用了非绑定的 style 属性,则会使用 parseStyleText 函数对属性值进行处理。在我们这个案例中模板中使用了非绑定的 style 属性,属性值为字符串 'color: red; 'parseStyleText 函数会把这个字符串解析为对象形式,如下:

{
  color: 'red'
}
1
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
})
1
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)
}
1
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 逻辑很简单,就是更新一下inVPreinPre 的状态,以及执行 postTransforms 函数。

说明:关于 closeElement 我们在上面小节中已经做了详细分析,请查看 这里 (opens new window)

# 4. 处理文本内容

​ 老规矩在分析之前我们还是以一个案例来分析,源码如下:

<div style="color:red;">{{ val }}  111</div>
1

​ 我们先来看一下下面代码,如下:

​ 源码目录: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
  }
  /* 省略... */
}
1
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>
1
2
3
  • 第二:文本节点在根元素的外面
<template>
  <div>根元素内的文本节点</div>根元素外的文本节点
</template>
1
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
  }
  /* 省略... */
}
1
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>
1
2
3

​ 如上 html 片段存在一个 <textarea> 标签,该标签拥有 placeholder 属性,但却没有真实的文本内容,假如我们使用如下代码获取字符串内容:

document.getElementById('box').innerHTML
1

​ 在 IE 浏览器中将得到如下字符串:

'<textarea placeholder="some placeholder...">some placeholder...</textarea>'
1

​ 可以看到 <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 ? ' ' : ''
  }
  /* 省略... */
}
1
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>
1

​ 接下来判断空白处理策略( 'preserve' | 'condense' )是否存在,首先我们需要明白一点程序执行到这里的前提是,不存在于 <pre> 标签的空白符,当前节点存在兄弟节点。如果存在则判断是否为 'condense',如果是,则通过 lineBreakRE 匹配换行符或回车符,匹配到将 text 变量设置为空字符串,匹配不到将 text 变量设置为' ',例如:

// lineBreakRE能匹配到换行符或回车符
<div><span></span>
</div>

// lineBreakRE不能能匹配到换行符或回车符
<div><span></span> </div>
1
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)
    }
  }
}
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
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 的元素描述对象拥有三个特殊的属性,分别是 expressiontokens 以及 text,其中 text 就是原始的文本内容,而 expressiontokens 的值是通过 parseText 函数解析的结果中读取的。

总结:

  • 1、如果文本节点存在于 v-pre 标签中,则会被作为普通文本节点对象
  • 2、<pre> 标签内的空白会被保留
  • 3、preserveWhitespace 只会保留那些不在开始标签之后的空格(说空白也没问题)
  • 4、普通文本节点的元素描述对象的类型为 3,即 type = 3
  • 5、包含字面量表达式的文本节点不会被作为普通的文本节点对待,而是会使用 parseText 函数解析它们,并创建一个类型为 2,即 type = 2 的元素描述对象

# 4.1 decodeHTMLCached

对于decodeHTMLCached 函数解码文本,来看如下代码:

<pre>
  &lt;div&gt;我是一个DIV&lt;/div&gt;
</pre>
1
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
  }
}
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
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)
  }
}
1
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,目的就是用来与普通文本节点作区分的。