vue源码分析(十一) 编译之解析(parse)——parse

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

# 1. 概述

​ 分析完 parseHTML 我们再回到 scr/compiler/parse/index.js ,继续分析 parse 的源代码。

​ 分析 parse 源码,我们还是以 第九章 (opens new window) 的案例代码为例进行,如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>parse</title>
  <script src="./vue.js"></script>
</head>
<body>
  <div id="app">{{ fullName }}</div>
    <script>
      new Vue({
        el: '#app',
        template: `
          <ul :class="classObject" class="list" v-show="isShow">
            <li v-for="(l, i) in list" :key="i" @click="clickItem(index)">{{ i }}:{{ l }}</li>
          </ul>
        `,
        data: {
          isShow: true,
          list: ['Vue', 'React', 'Angular'],
          classObject: {
            active: true,
            'text-danger': false
          }
        },
        methods: {
          clickItem(index) {
            console.log(index)
          }
        }
      })
    </script>
</body>
</html>
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

# 2. 整体结构

​ 在分析源码之前我们,先以为代码的方式梳理一下 parse 的整理结构,如下:

export function createASTElement (
  tag: string,
  attrs: Array<Attr>,
  parent: ASTElement | void
): ASTElement {
  /* 省略... */
}

// HTML字符串转换为AST
export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  /*
   * 省略...
   * 省略的代码用来初始化一些变量的值,以及创建一些新的变量,其中包括 root 变量,该变量为 parse 函数的返回值,即 AST
   */
  
  // 警告日志函数
  function warnOnce (msg, range) {
    /* 省略... */
  }

	// 关闭节点
  function closeElement (element) {
    /* 省略... */
  }

	// 删除尾部空白节点
	function trimEndingWhitespace (el) {
    /* 省略... */
  }

	// 校验根节点
	function checkRootConstraints (el) {
    /* 省略... */
  }

  parseHTML(template, {
    // 其他选项...
    start (tag, attrs, unary, start, end) {
      /* 省略... */
    },

    end (tag, start, end) {
      /* 省略... */
    },

    chars (text: string, start: number, end: number) {
      /* 省略... */
    },
    comment (text: string, start, end) {
      /* 省略... */
    }
  })
  return root
}

// 处理 v-pre 
function processPre (el) {/* 省略...*/}
function processRawAttrs (el) {/* 省略...*/}
// 处理 element
export function processElement (element: ASTElement, options: CompilerOptions) {/* 省略...*/}
// 处理 v-key
function processKey (el) {/* 省略...*/}
// 处理 ref
function processRef (el) {/* 省略...*/}
// 处理 v-for
export function processFor (el: ASTElement) {/* 省略...*/}
// 解析 v-for
export function parseFor (exp: string): ?ForParseResult {/* 省略...*/}
// 处理 v-if
function processIf (el) {/* 省略...*/}
// 处理 if 条件
function processIfConditions (el, parent) {/* 省略...*/}
// 找到 v-pre 中的值
function findPrevElement (children: Array<any>): ASTElement | void {/* 省略...*/}
// v-if的条件数组添加
export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {/* 省略...*/}
// 处理 v-once
function processOnce (el) {/* 省略...*/}
// 处理 slot
function processSlotContent (el) {/* 省略...*/}
function getSlotName (binding) {/* 省略...*/}
function processSlotOutlet (el) {/* 省略...*/}
// 处理 is 特性
function processComponent (el) {/* 省略...*/}
// 处理 attrs 熟悉
function processAttrs (el) {/* 省略...*/}
// 检查是否在 v-for中
function checkInFor (el: ASTElement): boolean {/* 省略...*/}
function parseModifiers (name: string): Object | void {/* 省略...*/}
function makeAttrsMap (attrs: Array<Object>): Object {/* 省略...*/}
// 是否是 text 标签,即script,style标签,不会解析
function isTextTag (el): boolean {/* 省略...*/}
// 是否是禁用标签
function isForbiddenTag (el): boolean {/* 省略...*/}
// 修复ie svg的bug
function guardIESVGBug (attrs) {/* 省略...*/}
// 检查v-model在for循环中的绑定的检查
function checkForAliasModel (el, value) {/* 省略...*/}
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

​ 分析完 src/compiler/parser/index.js 文件的整体结构。接下来我们回过头来,从文件的开始部分来分析。

# 3. 正则常量分析

​ 下面我们逐一分析一下这一系列常量。

# 3.1 onRE

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

export const onRE = /^@|^v-on:/
1

​ 这个常量用来匹配以字符 @v-on: 开头的字符串,主要作用是检测标签属性名是否是监听事件的指令。

# 3.2 dirRE

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

export const dirRE = process.env.VBIND_PROP_SHORTHAND
  ? /^v-|^@|^:|^\.|^#/
  : /^v-|^@|^:|^#/
1
2
3

​ 它用来匹配以字符 v-@:.# 开头的字符串,主要作用是检测标签属性名是否是指令。所以通过这个正则我们可以知道,在 vue 中所有以 v- 开头的属性都被认为是指令,另外 @ 字符是 v-on 的缩写,: 字符是 v-bind 的缩写。

# 3.3 forAliasRE

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

export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
1

​ 该正则包含三个分组,

  • 第一个分组为 ([\s\S]*?),该分组是一个惰性匹配的分组,\s 空白符 \S 非空白符,即它匹配的内容为任何字符,包括换行符等。
  • 第二个分组为 (?:in|of),该分组用来匹配字符串 in 或者 of,并且该分组是非捕获的分组。
  • 第三个分组为 ([\s\S]*),与第一个分组类似,不同的是第三个分组是非惰性匹配。

​ 同时每个分组之间都会匹配至少一个空白符 \s+。通过以上说明可知,正则 forAliasRE 用来匹配 v-for 属性的值,并捕获 inof 前后的字符串。假设我们像如下这样使用 v-for<div v-for="obj of list"></div> ,那么正则 forAliasRE 用来匹配字符串 'obj of list',并捕获到两个字符串 'obj''list'

# 3.4 forIteratorRE

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

export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
1

​ 该正则用来匹配 forAliasRE 包含两个分组:

  • 第一个捕获组用来捕获一个不包含字符 ,``}] 的字符串,且该字符串前面有一个字符 ,,如:', index'
  • 第二个分组为非捕获的分组,第三个分组为捕获的分组,其捕获的内容与第一个捕获组相同。

​ 举几个例子,我们知道 v-for 有几种不同的写法,其中一种使用 v-for 的方式是:

<div v-for="obj of list"></div>
1

​ 如果像如上这样使用 v-for,那么 forAliasRE 正则的第一个捕获组的内容为字符串 'obj',此时使用 forIteratorRE 正则去匹配字符串 'obj' 将得不到任何内容。

​ 第二种使用 v-for 的方式为:

<div v-for="(obj, index) of list"></div>
1

​ 此时 forAliasRE 正则的第一个捕获组的内容为字符串 '(obj, index)',如果去掉左右括号则该字符串为 'obj, index',如果使用 forIteratorRE 正则去匹配字符串 'obj, index' 则会匹配成功,并且 forIteratorRE 正则的第一个捕获组将捕获到字符串 'index',但第二个捕获组捕获不到任何内容。

​ 第三种使用 v-for 的方式为:

<div v-for="(value, key, index) in object"></div>
1

​ 以上方式主要用于遍历对象而非数组,此时 forAliasRE 正则的第一个捕获组的内容为字符串 '(value, key, index)',如果去掉左右括号则该字符串为 'value, key, index',如果使用 forIteratorRE 正则去匹配字符串 'value, key, index' 则会匹配成功,并且 forIteratorRE 正则的第一个捕获组将捕获到字符串 'key',但第二个捕获组将捕获到字符串 'index'

# 3.5 stripParensRE

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

const stripParensRE = /^\(|\)$/g
1

​ 用来匹配要么以字符 ( 开头,要么以字符 ) 结尾的字符串,或者两者都满足。例如在 v-for 中去除括号。

# 3.6 dynamicArgRE

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

const dynamicArgRE = /^\[.*\]$/
1

​ 用来匹配以字符 [ 开头并以字符 ] 结尾的字符串,作用是判断是否为动态属性。

. :匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用 \.

* :匹配前面的子表达式零次或多次。要匹配 * ,请使用 \*

# 3.7 argRE

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

const argRE = /:(.*)$/
1

​ 正则 argRE 用来匹配指令中的参数,如下:

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

​ 其中 v-on 为指令,click 为传递给 v-on 指令的参数,stop 为修饰符。所以 argRE 正则用来匹配指令编写中的参数,并且拥有一个捕获组,用来捕获参数的名字。

# 3.8 bindRE

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

export const bindRE = /^:|^\.|^v-bind:/
1

​ 该正则用来匹配以字符 : 或字符串 v-bind: 或字符串 . 开头的字符串,主要用来检测一个标签的属性是否是绑定(v-bind)。

# 3.9 propBindRE

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

const propBindRE = /^\./
1

​ 该正则用来匹配以符串 . 开头的字符串,主要用来检测一个(v-bind)指令是否绑定修饰符(.prop)。

说明:关于 .prop 请参考 v-bind (opens new window)

# 3.10 modifierRE

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

const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g
1

​ 用来匹配以字符 (v-bind)指令是否绑定修饰符,并且捕获匹配到的字符串,如下例子:

<svg :view-box.camel="viewBox"></svg>
1

​ 在代码中经过其他过滤,用来匹配此正则表达式的字符串为 :view-box.camel,所以最终匹配到的是 .camel

# 3.11 slotRE

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

const slotRE = /^v-slot(:|$)|^#/
1

​ 用来匹配以字符 (v-slot) 或 字符 # 开头的字符串,并且捕获匹配到的字符串中的 : 字符。

# 3.12 lineBreakRE

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

const lineBreakRE = /[\r\n]/
1

​ 匹配换行符和回车符。

# 3.13 whitespaceRE

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

const whitespaceRE = /\s+/g
1

​ 匹配任何空白字符一次或多次,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。注意Unicode 正则表达式会匹配全角空格符。

# 3.14 invalidAttributeRE

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

const invalidAttributeRE = /[\s"'<>\/=]/
1

​ 匹配 空白 或 "'<>/=字符。

# 3.15 解码函数

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

const decodeHTMLCached = cached(he.decode)
1

cached 作用是接收一个函数作为参数并返回一个新的函数,新函数的功能与作为参数传递的函数功能相同,唯一不同的是新函数具有缓存值的功能,如果一个函数在接收相同参数的情况下所返回的值总是相同的,那么 cached 函数将会为该函数提供性能提升的优势。

​ 可以看到传递给 cached 函数的参数是 he.decode 函数,其中 he 为第三方的库,he.decode 函数用于 HTML 字符实体的解码工作,如:

console.log(he.decode('&#x26;'))  // &#x26; -> '&'
1

​ 由于字符实体 & 代表的字符为 &。所以字符串 & 经过解码后将变为字符 &decodeHTMLCached 函数在后面将被用于对纯文本的解码,如果不进行解码,那么用户将无法使用字符实体编写字符。

# 3.16 平台化选项变量

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

// configurable state
// 日志输出函数
export let warn: any
// 改变纯文本插入分隔符。修改指令的书写风格,比如默认是{{mgs}}  delimiters: ['${', '}']之后变成这样 ${mgs}
let delimiters
// transforms 样式属性的集合函数
let transforms
// transforms  arr属性的集合 函数
let preTransforms
// 空数组 
let postTransforms
//  判断标签是否是pre 如果是则返回真
let platformIsPreTag
//  来检测一个属性在标签中是否要使用元素对象原生的 prop 进行绑定
let platformMustUseProp
// 来获取元素(标签)的命名空间,即判断 tag 是否是svg或者math 标签
let platformGetTagNamespace
// 判断是组件
let maybeComponent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 4. createASTElement 函数

createASTElement 函数的作用就是方便我们创建一个节点,或者说方便我们创建一个元素的描述对象,如下:

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

export function createASTElement (
  tag: string,
  attrs: Array<ASTAttr>,
  parent: ASTElement | void
): ASTElement {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent,
    children: []
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

​ 此函数接收三个参数,我们以概述里面的案例来分别是讲解,如下:

  • tag:标签名字 tag,如 ul
  • attrs:标签拥有的属性数组, 如 [{name: ':class', value: 'classObject'}, {name: 'class', value: 'list'}, {name: 'v-show', value: 'isShow'}]
  • parent:标签的父标签描述对象。

​ 返回一个对象,其中 attrsMap 是通过调用 makeAttrsMap(attrs) 函数得到的,下面我们看一下 makeAttrsMap 的源码,如下:

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

function makeAttrsMap (attrs: Array<Object>): Object {
  const map = {}
  for (let i = 0, l = attrs.length; i < l; i++) {
    if (
      process.env.NODE_ENV !== 'production' &&
      map[attrs[i].name] && !isIE && !isEdge
    ) {
      warn('duplicate attribute: ' + attrs[i].name, attrs[i])
    }
    map[attrs[i].name] = attrs[i].value
  }
  return map
}
1
2
3
4
5
6
7
8
9
10
11
12
13

​ 可以看出 makeAttrsMap 函数的作用就是通过循环属性数组将标签的属性数组转换成健值对。例如:[{name: ':class', value: 'classObject'}, {name: 'class', value: 'list'}, {name: 'v-show', value: 'isShow'}] 这个属性数组最终经过 makeAttrsMap 函数转换为:{':class': 'classObject', 'class': 'list', 'v-show': 'isShow'}

​ 我们还是以概述里面的案例来分析,通过 createASTElement 函数生成最终对象的结构。

<ul :class="classObject" class="list" v-show="isShow">
    <li v-for="(l, i) in list" :key="i" @click="clickItem(index)">{{ i }}:{{ l }}</li>
</ul>
1
2
3
  • 其中 ul 节点转换后的值如下:
{
  "type":1,
   "tag":"ul",
   "attrsList":[
     {
       "name":":class",
       "value":"classObject",
       "start":4,
       "end":24
     },{
       "name":"class",
       "value":"list",
       "start":25,
       "end":37
     },{
       "name":"v-show",
       "value":"isShow",
       "start":38,
       "end":53
     }
   ],
   "attrsMap":{
     ":class":"classObject",
     "class":"list",
     "v-show":"isShow"
   },
   "rawAttrsMap":{},
   "children":[]
}
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
  • 其中 li 节点转换后的值如下:
{
  "type":1,
  "tag":"li",
  "attrsList":[
    {
      "name":"v-for",
      "value":"(l, i) in list",
      "start":71,
      "end":93
    },{
      "name":":key",
      "value":"i",
      "start":94,
      "end":102
    },{
      "name":"@click",
      "value":"clickItem(index)",
      "start":103,
      "end":128
    }
  ],
  "attrsMap":{
    "v-for":"(l, i) in list",
    ":key":"i",
    "@click":"clickItem(index)"
  },
  "rawAttrsMap":{},
  "parent":{
     "type":1,
     "tag":"ul",
     "attrsList":[
       {
         "name":":class",
         "value":"classObject",
         "start":4,
         "end":24
       },{
         "name":"class",
         "value":"list",
         "start":25,
         "end":37
       },{
         "name":"v-show",
         "value":"isShow",
         "start":38,
         "end":53
       }
     ],
     "attrsMap":{
       ":class":"classObject",
       "class":"list",
       "v-show":"isShow"
     },
     "rawAttrsMap":{},
     "children":[]
  },
  "children":[]
}
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

# 5. parse

​ 在分析 parse 之前我们先整理一下 parse 的整体结构,如下:

export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  // 平台化选项变量
  
  // 其他变量
  
  // 警告日志函数
  function warnOnce (msg, range) {
    /* 省略... */
  }

	// 关闭节点
  function closeElement (element) {
    /* 省略... */
  }

	// 删除尾部空白节点
	function trimEndingWhitespace (el) {
    /* 省略... */
  }

	// 校验根节点
	function checkRootConstraints (el) {
    /* 省略... */
  }

  parseHTML(template, {
    // 其他选项...
    start (tag, attrs, unary, start, end) {
      /* 省略... */
    },

    end (tag, start, end) {
      /* 省略... */
    },

    chars (text: string, start: number, end: number) {
      /* 省略... */
    },
    comment (text: string, start, end) {
      /* 省略... */
    }
  })
  return root
}
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

# 5.1 变量

​ 在分析标签处理之前我们先看一下一些变量的作用,如下:

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

// 平台化选项
warn = options.warn || baseWarn 
platformIsPreTag = options.isPreTag || no /
platformMustUseProp = options.mustUseProp || no
platformGetTagNamespace = options.getTagNamespace || no
const isReservedTag = options.isReservedTag || no
maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)

transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')

delimiters = options.delimiters

const preserveWhitespace = options.preserveWhitespace !== false
const whitespaceOption = options.whitespace

// 其他变量
const stack = []
let root 
let currentParent 
let inVPre = false 
let inPre = false 
let warned = false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • warn:用来打印警告信息的
  • platformIsPreTag:判断标签是否是 pre 标签
  • platformMustUseProp:用来检测一个属性在标签中是否要使用元素对象原生的 prop 进行绑定,注意:这里的 prop 指的是元素对象的属性,而非 Vue 中的 props 概念
  • platformGetTagNamespace:用来获取元素(标签)的命名空间
  • isReservedTag:判断标签是否是保留的标签
  • maybeComponent:判断是否为组件
  • transforms

​ 我们前面分析过 options 的值,知道 options.module 值如下:

options.modules = [
  {
    staticKeys: ['staticClass'],
    transformNode,
    genData
  },
  {
    staticKeys: ['staticStyle'],
    transformNode,
    genData
  },
  {
    preTransformNode
  }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

​ 我们再来看看 pluckModuleFunction 函数的定义,如下:

​ 源码目录:src/compiler/helpers.js

export function pluckModuleFunction<F: Function> (
  modules: ?Array<Object>,
  key: string
): Array<F> {
  return modules
    ? modules.map(m => m[key]).filter(_ => _)
    : []
}
1
2
3
4
5
6
7
8

pluckModuleFunction 函数的作用是从第一个参数中"采摘"出函数名字与第二个参数所指定字符串相同的函数,并将它们组成一个数组。

​ 源码中 transforms 的值的获取代码如下:

transforms = pluckModuleFunction(options.modules, 'transformNode')
1

​ 调用这句代码,我们分两步来看,第一步是 map ,即如下:

options.modules.map(m => m['transformNode'])
1

​ 所以第一步通过 map 遍历后的值为:

[
  transformNode,
  transformNode,
  undefined
]
1
2
3
4
5

​ 接着我们继续看第二步,如下:

[
  transformNode,
  transformNode,
  undefined
].filter(_ => _)
1
2
3
4
5

filter 的作用是过滤掉 undefined ,所以最终得到的 transforms 为,如下:

[
  transformNode,
  transformNode
]
1
2
3
4
  • preTransforms:同 transforms,所以最终得到的 preTransforms为 :
[ preTransformNode ]
1
  • postTransforms:同 transforms,所以最终得到的 postTransforms 为 :
[]
1
  • delimiters:改变纯文本插入分隔符。修改指令的书写风格,比如默认是 delimiters: ['${', '}']之后变成这样 ${mgs}
  • preserveWhitespace:判断是否保留元素之间的空白,用来告诉编译器在编译 html 字符串时是否放弃标签之间的空格,如果为 true 则代表放弃
  • whitespaceOption:空白处理策略,'preserve' | 'condense'
  • stack:是用来修正当前正在解析元素的父级
  • root:定义AST模型对象
  • currentParent:描述对象之间的父子关系
  • inVPre:标识当前解析的标签是否在拥有 v-pre 的标签之内
  • inPre:标识当前正在解析的标签是否在 <pre></pre> 标签之内
  • warned:标识只会打印一次警告信息,默认为 false

# 5.2 处理标签

说明:关于 parse 中处理标签我们会在 第十二章 (opens new window) 做详细分析。

# 6. 参考资料

JS正则表达式一条龙 (opens new window)

正则表达式图解 (opens new window)

Vue parse之 从template到astElement 源码详解 (opens new window)