## 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` 的值,用一张图表示将会更加清晰: ![](http://ovjvjtt4l.bkt.clouddn.com/2017-12-26-115232.jpg) 可以看到,最终获取到的内容是不包含注释节点的起始(``)的。 这样一个注释节点就 `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`,还是用一张图来表示: ![](http://ovjvjtt4l.bkt.clouddn.com/2017-12-26-121606.jpg) 可以很容易的看到,经过 `advance` 函数后,新的 `html` 字符串将从 `commentEnd + 3` 的位置开始,而不再包含已经 `parse` 过的注释节点了。 最后还有一个很重要的步骤,即调用完 `advance` 函数之后,要执行 `continue` 跳过此次循环,由于此时 `html` 字符串已经是去掉了 `parse` 过的部分的新字符串了,所以开启下一次循环,重新开始 `parse` 过程。