打开 src/compiler/parser/html-parser.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,实际上,我们上一小节所讲的小例子就是在这个项目的基础上所做的修改。Vue
在此基础上做了很多完善的工作,下面我们就探究一下 Vue
中的 html parser
都做了哪些事情。
代码正文的一开始,是两句 import
语句,以及定义的一些正则常量:
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 = /^<!DOCTYPE [^>]+>/i
const comment = /^<!--/
const conditionalComment = /^<!\[/
这与上之前我们讲解的小例子中所定义的正则的作用基本是一致的,只不过 Vue
所定义的正则更加严谨和完善,我们一次看一下这些正则的作用。首先看一下 attribute
常量:
// Regular Expressions for parsing tags and attributes
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
attribute
顾名思义,这个正则的作用是用来匹配标签的属性(attributes
)的,如下图所示:
我们在观察一个复杂表达式的时候,主要就是要观察它有几个分组(准确的说应该是有几个捕获的分组),通过上图我们能够清晰的看到,这个表达式一个有五个捕获组,第一个捕获组用来匹配属性名,第二个捕获组用来匹配等于号,第三、第四、第五个捕获组都是用来匹配属性名的,这是因为在 html
标签中有三种写属性值的方式:
class="some-class"
class='some-class'
class=some-class
正因如此,需要三个正则分组分别匹配三种情况,我们可以对这个正则做一个测试,如下:
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)) // 测试无引号
对于双引号的情况,我们将得到以下结果:
[
'class="some-class"',
'class',
'=',
'some-class',
undefined,
undefined
]
数组共有从 0
到 5
六个元素,第 0
个元素是被整个正则所匹配的结果,从第 1
至第 5
个元素分别对应五个捕获组的匹配结果,我们可以看到,第 1
个元素对应第一个捕获组,匹配到了属性名(class
);第 2
个元素对应第二个捕获组,匹配到了等号(=
);第 3
个元素对应第三个捕获组,匹配到了带双引号的属性值;而第 4
和第 5
个元素分别对应第四和第五个捕获组,由于没有匹配到所以都是 undefined
。
所以通过以上结果我们很容易想到当属性值被单引号起来和不使用引号的情况,所得到的匹配结果是什么,变化主要就在数组的第 3
、4
、5
个元素,匹配到哪种情况,那么对应的位置就是属性值,其他位置则是 undefined
,如下:
// 对于单引号的情况
[
'class="some-class"',
'class',
'=',
undefined,
'some-class',
undefined
]
// 对于没有引号
[
'class="some-class"',
'class',
'=',
undefined,
undefined,
'some-class'
]
接下来一句代码如下:
// 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 中,标签是用户自己定义的,比如:<bug></bug>
。
正因为这样,所以不同的文档中如果定义了相同的元素(标签),就会产生冲突,为此,XML 允许用户为标签指定前缀:<k:bug></k:bug>
,前缀是字母 k
。
除了前缀还可以使用命名空间,即使用标签的 xmlns
属性,为前缀赋予与指定命名空间相关联的限定名称:
<k:bug xmlns:k="http://www.xxx.com/xxx"></k:bug>
综上所述,一个合法的XML标签名应该是由 前缀
、冒号(:)
以及 标签名称
组成的:<前缀:名称>
ncname
?ncname
的全称是 An XML name that does not contain a colon (:)
即:不包含冒号(:
)的 XML 名称。也就是说 ncname
就是不包含前缀的XML标签名称。大家可以在这里找到关于 ncname 的概念。
qname
?我们可以在 Vue
的源码中看到其给出了一个连接:https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName,其实 qname
就是:<前缀:名称>
,也就是合法的XML标签。
了解了这些,我们再来看 ncname
的正则表达式,它定了 ncname
的合法组成,这个正则所匹配的内容很简单:*字母、数字或下划线开头,后面可以跟任意数量的字符、中横线和 .
*。