# Vue 中的 html-parser
本节中大量出现 `parse` 以及 `parser` 这两个单词,不要混淆这两个单词,`parse` 是动词,代表“解析”的过程,`parser` 是名词,代表“解析器”。
打开 `src/compiler/parser/html-parser.js` 文件,该文件的开头是一段注释:
```js
/**
* Not type-checking this file because it's mostly vendor code.
*/
/*!
* HTML Parser By John Resig (ejohn.org)
* Modified by Juriy "kangax" Zaytsev
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*/
```
通过这段注释我们可以了解到,`Vue` 的 `html parser` 的灵感来自于 [John Resig 所写的一个开源项目:http://erik.eae.net/simplehtmlparser/simplehtmlparser.js](http://erik.eae.net/simplehtmlparser/simplehtmlparser.js),实际上,我们上一小节所讲的小例子就是在这个项目的基础上所做的修改。`Vue` 在此基础上做了很多完善的工作,下面我们就探究一下 `Vue` 中的 `html parser` 都做了哪些事情。
## 正则分析
代码正文的一开始,是两句 `import` 语句,以及定义的一些正则常量:
```js
import { makeMap, no } from 'shared/util'
import { isNonPhrasingTag } from 'web/compiler/util'
// Regular Expressions for parsing tags and attributes
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
// but for Vue templates we can enforce a simple charset
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^]+>/i
// #7298: escape - to avoid being pased as HTML comment when inlined in page
const comment = /^\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
```
`attribute` 顾名思义,这个正则的作用是用来匹配标签的属性(`attributes`)的,如下图所示:

我们在观察一个复杂表达式的时候,主要就是要观察它有几个分组(准确的说应该是有几个捕获的分组),通过上图我们能够清晰的看到,这个表达式有五个捕获组,第一个捕获组用来匹配属性名,第二个捕获组用来匹配等于号,第三、第四、第五个捕获组都是用来匹配属性值的,这是因为在 `html` 标签中有三种写属性值的方式:
* 1、使用双引号把值引起来:`class="some-class"`
* 2、使用单引号把值引起来:`class='some-class'`
* 3、不使用引号:`class=some-class`
正因如此,需要三个正则分组分别匹配三种情况,我们可以对这个正则做一个测试,如下:
```js
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
console.log('class="some-class"'.match(attribute)) // 测试双引号
console.log("class='some-class'".match(attribute)) // 测试单引号
console.log('class=some-class'.match(attribute)) // 测试无引号
```
对于双引号的情况,我们将得到以下结果:
```js
[
'class="some-class"',
'class',
'=',
'some-class',
undefined,
undefined
]
```
数组共有从 `0` 到 `5` 六个元素,第 `0` 个元素是被整个正则所匹配的结果,从第 `1` 至第 `5` 个元素分别对应五个捕获组的匹配结果,我们可以看到,第 `1` 个元素对应第一个捕获组,匹配到了属性名(`class`);第 `2` 个元素对应第二个捕获组,匹配到了等号(`=`);第 `3` 个元素对应第三个捕获组,匹配到了带双引号的属性值;而第 `4` 和第 `5` 个元素分别对应第四和第五个捕获组,由于没有匹配到所以都是 `undefined`。
所以通过以上结果我们很容易想到当属性值被单引号起来和不使用引号的情况,所得到的匹配结果是什么,变化主要就在匹配结果数组的第 `3`、`4`、`5` 个元素,匹配到哪种情况,那么对应的位置就是属性值,其他位置则是 `undefined`,如下:
```js
// 对于单引号的情况
[
'class="some-class"',
'class',
'=',
undefined,
'some-class',
undefined
]
// 对于没有引号
[
'class="some-class"',
'class',
'=',
undefined,
undefined,
'some-class'
]
```
### ncname
接下来一句代码如下:
```js
// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
// but for Vue templates we can enforce a simple charset
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
```
首先给大家解释几个概念并说明一些问题:
* 一、合法的 XML 名称是什么样的?
首先在 XML 中,标签是用户自己定义的,比如:`
`,如果不是一元标签,此时就应该是:`>`
观察 `startTagClose` 可知,这个正则拥有一个捕获分组,用来捕获开始标签结束部分的斜杠:`/`。
### endTag
```js
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
```
`endTag` 这个正则用来匹配结束标签,由于该正则同样使用了字符串 `qnameCapture`,所以这个正则也拥有了一个捕获组,用来捕获标签名称。
### doctype
```js
const doctype = /^]+>/i
```
这个正则用来匹配文档的 `DOCTYPE` 标签,没有捕获组。
### comment
```js
// #7298: escape - to avoid being pased as HTML comment when inlined in page
const comment = /^',
'"': '"',
'&': '&',
'
': '\n',
' ': '\t'
}
const encodedAttr = /&(?:lt|gt|quot|amp);/g
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#10|#9);/g
```
上面这段代码中,包含 `5` 个常量,我们逐个来看。
首先是 `isPlainTextElement` 常量是一个函数,它是通过 `makeMap` 函数生成的,用来检测给定的标签名字是不是纯文本标签(包括:`script`、`style`、`textarea`)。
然后定义了 `reCache` 常量,它被初始化为一个空的 `JSON` 对象字面量。
再往下定义了 `decodingMap` 常量,它也是一个 `JOSN` 对象字面量,其中 `key` 是一些特殊的 `html` 实体,值则是这些实体对应的字符。在 `decodingMap` 常量下面的是两个正则常量:`encodedAttr` 和 `encodedAttrWithNewLines`。可以发现正则 `encodedAttrWithNewLines` 会比 `encodedAttr` 多匹配两个 `html` 实体字符,分别是 `
` 和 ` `。对于 `decodingMap` 以及下面两个正则的作用不知道大家能不能猜得到,其实我们 [创建编译器](http://localhost:8080/#/note/7Vue%E7%9A%84%E7%BC%96%E8%AF%91%E5%99%A8%E5%88%9D%E6%8E%A2) 一节中有讲到 `shouldDecodeNewlines` 和 `shouldDecodeNewlinesForHref` 这两个编译器选项,当时我们就有针对这两个选项的作用做讲解,可以在附录 [platforms/web/util 目录下的工具方法全解](http://localhost:8080/#/note/%E9%99%84%E5%BD%95/web-util?id=compat-js-%E6%96%87%E4%BB%B6) 中查看。
所以这里的常量 `decodingMap` 以及两个正则 `encodedAttr` 和 `encodedAttrWithNewLines` 的作用就是用来完成对 `html` 实体进行解码的。
再往下是这样一段代码:
```js
// #5992
const isIgnoreNewlineTag = makeMap('pre,textarea', true)
const shouldIgnoreFirstNewline = (tag, html) => tag && isIgnoreNewlineTag(tag) && html[0] === '\n'
```
定义了两个常量,其中 `isIgnoreNewlineTag` 是一个通过 `makeMap` 函数生成的函数,用来检测给定的标签是否是 `
` 标签或者 `