## Vue 中的 html-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
const comment = /^`
* 2、可能是条件注释节点:``
* 3、可能是 `doctype`:``
* 4、可能是结束标签:``
* 5、可能是开始标签:``
* 6、可能只是一个单纯的字符串:`')
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
advance(commentEnd + 3)
continue
}
}
```
对于注释节点的判断方法是使用正则常量 `comment` 进行判断,即:`comment.test(html)`,对于 `comment` 正则常量我们在前面分析正则的部分已经讲过了,当时我提醒过大家一件事情,即这些正则常量有一个共同的特点:**都是从字符串的开头位置开始匹配的**,也就是说只有当 `html` 字符串的第一个字符是左尖括号(`<`)时才有意义。而现在我们分析的情况恰好是当 `textEnd === 0`,也就是说 `html` 字符串的第一个字符确实是左尖括号(`<`)。
所以如果 `comment.test(html)` 条件为真,则说明**可能是**注释节点,大家要注意关键字:**可能是**,为什么这么说呢?大家知道完整的注释节点不仅仅要以 `` 结尾,如果只以 `` 结尾,那显然不是一个注释节点,所以首先要检查 `html` 字符串中 `-->` 的位置:
```js
const commentEnd = html.indexOf('-->')
```
如果找到了 `-->`,则说明这确实是一个注释节点,那么就处理之,否则什么事情都不做。处理的代码如下:
```js
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
advance(commentEnd + 3)
continue
```
首先判断 `parser` 选项 `options.shouldKeepComment` 是否为真,如果为真则调用同为 `parser` 选项的 `options.comment` 函数,并将注释节点的内容作为参数传递。在 `Vue` 官方文档中可以找到一个叫做 `comments` 的选项,实际上这里的 `options.shouldKeepComment` 的值就是 `Vue` 选项 `comments` 的值,这一点当我们讲到生成抽象语法树(`AST`)的时候即可看到。
回过头来我们再次查看以上代码,我们看看这里是如何获取注释内容的:
```js
html.substring(4, commentEnd)
```
通过调用字符串的 `substring` 方法截取注释内容,其中其实位置是 `4`,结束位置是 `commentEnd` 的值,用一张图表示将会更加清晰:

可以看到,最终获取到的内容是不包含注释节点的起始(``)的。
这样一个注释节点就 `parse` 完毕了,那么完毕之后应该做什么呢?要做的很关键的一件事就是:**将已经 `parse` 完毕的字符串剔除**,也就是接下来调用的 `advance` 函数:
```js
advance(commentEnd + 3)
```
该函数定义在 `while` 循环的下方,源码如下:
```js
function advance (n) {
index += n
html = html.substring(n)
}
```
`advance` 函数接收一个 `Number` 类型的参数 `n`,我们刚刚说到:已经 `parse` 完毕的部分要从 `html` 字符串中剔除,而剔除的方式很简单,就是找到已经 `parse` 完毕的字符串的结束位置,然后执行 `html = html.substring(n)` 即可,这里的 `n` 就是所谓的结束位置。除此之外,我们发现 `advance` 函数还对 `index` 变量做了赋值:`index += n`,前面我们介绍变量的时候说到过,`index` 变量存储着字符流的读入位置,该位置是相对于原始 `html` 字符串的,所以每次都要更新。
那么对于注释节点,其执行的代码为:
```js
advance(commentEnd + 3)
```
`n` 的值是 `commentEnd + 3`,还是用一张图来表示:

可以很容易的看到,经过 `advance` 函数后,新的 `html` 字符串将从 `commentEnd + 3` 的位置开始,而不再包含已经 `parse` 过的注释节点了。
最后还有一个很重要的步骤,即调用完 `advance` 函数之后,要执行 `continue` 跳过此次循环,由于此时 `html` 字符串已经是去掉了 `parse` 过的部分的新字符串了,所以开启下一次循环,重新开始 `parse` 过程。