|
@@ -1,6 +1,6 @@
|
|
|
# 词法分析 - 为生成AST做准备
|
|
|
|
|
|
-在 [Vue的编译器初探](./80vue-compiler-start.md) 这一章节中,我们对 `Vue` 如何创建编译器,以及在这个过程中经历过的几个重要的函数做了分析,比如 `compileToFunctions` 函数以及 `compile` 函数,并且我们知道真正对模板进行编译工作的实际是 `baseCompile` 函数,而接下来我们任务就是搞清楚 `baseCompile` 函数的内容。
|
|
|
+在 [Vue的编译器初探](./80vue-compiler-start.md) 这一章节中,我们对 `Vue` 如何创建编译器,以及在这个过程中经历过的几个重要的函数做了分析,比如 `compileToFunctions` 函数以及 `compile` 函数,并且我们知道真正对模板进行编译工作的实际是 `baseCompile` 函数,而接下来我们的任务就是搞清楚 `baseCompile` 函数的内容。
|
|
|
|
|
|
`baseCompile` 函数是在 `src/compiler/index.js` 中作为 `createCompilerCreator` 函数的参数使用的,代码如下:
|
|
|
|
|
@@ -78,7 +78,7 @@ const ast = parse(template.trim(), options)
|
|
|
const ast = parse(template.trim(), options)
|
|
|
```
|
|
|
|
|
|
-由这句代码可知 `parse` 函数就是用来解析模板字符串的,最终生成 `AST`,根据文件头部的引用关系可知 `parse` 函数 `src/compiler/parser/index.js` 文件,打开该文件可以发现其的确导出了一个名字为 `parse` 的函数,如下:
|
|
|
+由这句代码可知 `parse` 函数就是用来解析模板字符串的,最终生成 `AST`,根据文件头部的引用关系可知 `parse` 函数位于 `src/compiler/parser/index.js` 文件,打开该文件可以发现其的确导出了一个名字为 `parse` 的函数,如下:
|
|
|
|
|
|
```js
|
|
|
export function parse (
|
|
@@ -169,7 +169,7 @@ const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s
|
|
|
|
|
|

|
|
|
|
|
|
-我们在观察一个复杂的正则表达式时,主要就是要观察它有几个分组(准确的说应该是有几个捕获的分组),通过上图我们能够清晰的看到,这个表达式有五个捕获组,第一个捕获组用来匹配属性名,第二个捕获组用来匹配等于号,第三、第四、第五个捕获组都是用来匹配属性值的,同时 `?` 表明第三、四、五个分组是可选的。 这是因为在 `html` 标签中有4种写属性值的方式:
|
|
|
+我们在观察一个复杂的正则表达式时,主要就是要观察它有几个分组(准确地说应该是有几个捕获的分组),通过上图我们能够清晰地看到,这个表达式有五个捕获组,第一个捕获组用来匹配属性名,第二个捕获组用来匹配等于号,第三、第四、第五个捕获组都是用来匹配属性值的,同时 `?` 表明第三、四、五个分组是可选的。 这是因为在 `html` 标签中有4种写属性值的方式:
|
|
|
|
|
|
* 1、使用双引号把值引起来:`class="some-class"`
|
|
|
* 2、使用单引号把值引起来:`class='some-class'`
|
|
@@ -265,7 +265,7 @@ const ncname = '[a-zA-Z_][\\w\\-\\.]*'
|
|
|
|
|
|
* 三、什么是 `qname`?
|
|
|
|
|
|
-我们可以在 `Vue` 的源码中看到其给出了一个连接:[https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName](https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName),其实 `qname` 就是:`<前缀:标签名称>`,也就是合法的XML标签。
|
|
|
+我们可以在 `Vue` 的源码中看到其给出了一个链接:[https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName](https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName),其实 `qname` 就是:`<前缀:标签名称>`,也就是合法的XML标签。
|
|
|
|
|
|
了解了这些,我们再来看 `ncname` 的正则表达式,它定义了 `ncname` 的合法组成,这个正则所匹配的内容很简单:*字母或下划线开头,后面可以跟任意数量的字符、中横线和 `.`*。
|
|
|
|
|
@@ -368,7 +368,7 @@ const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#10|#9);/g
|
|
|
|
|
|
上面这段代码中,包含 `5` 个常量,我们逐个来看。
|
|
|
|
|
|
-首先是 `isPlainTextElement` 常量是一个函数,它是通过 `makeMap` 函数生成的,用来检测给定的标签名字是不是纯文本标签(包括:`script`、`style`、`textarea`)。
|
|
|
+首先 `isPlainTextElement` 常量是一个函数,它是通过 `makeMap` 函数生成的,用来检测给定的标签名字是不是纯文本标签(包括:`script`、`style`、`textarea`)。
|
|
|
|
|
|
然后定义了 `reCache` 常量,它被初始化为一个空的 `JSON` 对象字面量。
|
|
|
|
|
@@ -957,7 +957,7 @@ match.unarySlash = end[1]
|
|
|
|
|
|
我们发现只有当变量 `end` 存在时,即能够确定确实解析到了一个开始标签的时候 `parseStartTag` 函数才会有返回值,并且返回值是 `match` 对象,其他情况下 `parseStartTag` 全部返回 `undefined`。
|
|
|
|
|
|
-下面我们整理一下 `parseStartTag` 函数的返回值,即 `match` 对象。当成功的匹配到一个开始标签时,假设有如下 `html` 字符串:
|
|
|
+下面我们整理一下 `parseStartTag` 函数的返回值,即 `match` 对象。当成功地匹配到一个开始标签时,假设有如下 `html` 字符串:
|
|
|
|
|
|
```html
|
|
|
<div v-if="isSucceed" v-for="v in map"></div>
|
|
@@ -1068,7 +1068,7 @@ const tagName = match.tagName
|
|
|
const unarySlash = match.unarySlash
|
|
|
```
|
|
|
|
|
|
-这两个常量的值都来自于开始标签的匹配结果,以下我们统一将开始标签的匹配结果称为 `match` 对象。其中常量 `tagName` 为开始标签的标签名,常量 `unarySlash` 的值为 `'/'` 或 `undefined` 其中之一。
|
|
|
+这两个常量的值都来自于开始标签的匹配结果,以下我们统一将开始标签的匹配结果称为 `match` 对象。其中常量 `tagName` 为开始标签的标签名,常量 `unarySlash` 的值为 `'/'` 或 `undefined` 。
|
|
|
|
|
|
接着是一个 `if` 语句块,`if` 语句的判断条件是 `if (expectHTML)`,前面说过 `expectHTML` 是 `parser` 选项,是一个布尔值,如果为真则该 `if` 语句块的代码将被执行。但是现在我们暂时不看这段代码,因为这段代码包含 `parseEndTag` 函数的调用,所以待我们讲解完 `parseEndTag` 函数之后,再回头来说这段代码。
|
|
|
|
|
@@ -1171,7 +1171,7 @@ if (options.start) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-如果 `parser` 选项中包含 `options.start` 函数,则调用之,并将开始标签的名字(`tagName`),格式化后的属性数组(`attrs`),是否为一元标签(`unary`),以及开始标签在元 `html` 中的开始和结束位置(`match.start` 和 `match.end`) 作为参数传递。
|
|
|
+如果 `parser` 选项中包含 `options.start` 函数,则调用之,并将开始标签的名字(`tagName`),格式化后的属性数组(`attrs`),是否为一元标签(`unary`),以及开始标签在原 `html` 中的开始和结束位置(`match.start` 和 `match.end`) 作为参数传递。
|
|
|
|
|
|
### parse 结束标签
|
|
|
|
|
@@ -1296,7 +1296,7 @@ function parseEndTag (tagName, start, end) {
|
|
|
|
|
|
当一个函数拥有两个及以上功能的时候,最常用的技巧就是通过参数进行控制,所以 `parseEndTag` 函数也不例外。`parseEndTag` 函数接收三个参数,这三个参数其实都是可选的,根据传参的不同其功能也不同。
|
|
|
|
|
|
-可以明确的告诉大家,在 `Vue` 的 `html-parser` 中 `parseEndTag` 函数的使用方式有三种:
|
|
|
+可以明确地告诉大家,在 `Vue` 的 `html-parser` 中 `parseEndTag` 函数的使用方式有三种:
|
|
|
|
|
|
* 第一种是处理普通的结束标签,此时**三个参数都传递**
|
|
|
* 第二种是只传递第一个参数:
|
|
@@ -1397,7 +1397,7 @@ if (tagName) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-用一句话描述上面这代码的作用:寻找当前解析的结束标签所对应的开始标签在 `stack` 栈中的位置。实现方式是如果 `tagName` 存在,则开启一个 `for` 循环从后向前遍历 `stack` 栈,直到找到相应的位置,并且该位置索引会保存到 `pos` 变量中,如果 `tagName` 不存在,则直接将 `pos` 设置为 `0`。
|
|
|
+用一句话描述上面这段代码的作用:寻找当前解析的结束标签所对应的开始标签在 `stack` 栈中的位置。实现方式是如果 `tagName` 存在,则开启一个 `for` 循环从后向前遍历 `stack` 栈,直到找到相应的位置,并且该位置索引会保存到 `pos` 变量中,如果 `tagName` 不存在,则直接将 `pos` 设置为 `0`。
|
|
|
|
|
|
那么 `pos` 变量是用来干什么的呢?实际上 `pos` 变量会被用来判断是否有元素缺少闭合标签。我们继续查看后面的代码就明白了,即:
|
|
|
|
|
@@ -1606,7 +1606,7 @@ if (options.chars && text) {
|
|
|
|
|
|
根据上例,此时 `text` 的值为字符串 `0<1`,所以这部分字符串将被作为普通字符串处理,如果 `options.chars` 存在,则会调用该钩子函数并将字符串传递过去。
|
|
|
|
|
|
-大家也许注意到了,原始的 `html` 被分拆为两部分,其中一部分为 `0<1`,这部分被作为普通文本对待,那么剩余的字符串 `<2` 呢?这部分字符串将会在下一次整体的 `while` 循环处理,此时由于 `html` 字符串的值将被更新为 `<2`,第一个字符为 `<`,所以该字符的索引为 `0`,这时既会匹配 `textEnd` 等于 `0` 的情况,也会匹配 `textEnd` 大于等于 `0` 的情况,但是由于字符串 `<2` 既不能匹配标签,也不会被 `textEnd` 大于等于 `0` 的 `if` 语句块处理,所以代码最终会来到这里:
|
|
|
+大家也许注意到了,原始的 `html` 被拆分为两部分,其中一部分为 `0<1`,这部分被作为普通文本对待,那么剩余的字符串 `<2` 呢?这部分字符串将会在下一次整体的 `while` 循环处理,此时由于 `html` 字符串的值将被更新为 `<2`,第一个字符为 `<`,所以该字符的索引为 `0`,这时既会匹配 `textEnd` 等于 `0` 的情况,也会匹配 `textEnd` 大于等于 `0` 的情况,但是由于字符串 `<2` 既不能匹配标签,也不会被 `textEnd` 大于等于 `0` 的 `if` 语句块处理,所以代码最终会来到这里:
|
|
|
|
|
|
```js
|
|
|
if (html === last) {
|
|
@@ -1660,7 +1660,7 @@ while (html) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-在这个 `while` 循环内有一个 `if...else` 语句块,代码被该 `if...else` 语句块分为两部分处理,前面我们所讲的都是 `if` 语句块内的代码,我们知道 `else` 语句块的代码只有当 `lastTag` 存在并且 `lastTag` 为纯文本标签是才会被执行,所以可想而知 `else` 语句块的代码就是用来处理纯文本标签内的内容的,什么是纯文本标签呢?根据 `isPlainTextElement` 函数可知纯文本标签包括 `script` 标签、`style` 标签以及 `textarea` 标签。
|
|
|
+在这个 `while` 循环内有一个 `if...else` 语句块,代码被该 `if...else` 语句块分为两部分处理,前面我们所讲的都是 `if` 语句块内的代码,我们知道 `else` 语句块的代码只有当 `lastTag` 存在并且 `lastTag` 为纯文本标签时才会被执行,所以可想而知 `else` 语句块的代码就是用来处理纯文本标签内的内容的,什么是纯文本标签呢?根据 `isPlainTextElement` 函数可知纯文本标签包括 `script` 标签、`style` 标签以及 `textarea` 标签。
|
|
|
|
|
|
下面我们就看一下它是如何处理纯文本标签的内容的,首先我们要明确的一点是 `else` 分支的代码处理的是纯文本标签的**内容**,并不是纯文本标签。假设我们的 `html` 字符串如下:
|
|
|
|
|
@@ -1686,7 +1686,7 @@ const stackedTag = lastTag.toLowerCase()
|
|
|
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
|
|
|
```
|
|
|
|
|
|
-变量 `endTagLength` 的初始值为 `0`,后面我们会看到 `endTagLength` 变量用来保存纯文本标签闭合标签的字符长度。`stackedTag` 常量的值为纯文本标签的小写版,`reStackedTag` 常量稍微复杂一些,它的值是一个正则表达式实例,并且使用 `reCache[stackedTag]` 做了缓存,我们看下一啊 `reStackedTag` 正则的作用是什么,如下:
|
|
|
+变量 `endTagLength` 的初始值为 `0`,后面我们会看到 `endTagLength` 变量用来保存纯文本标签闭合标签的字符长度。`stackedTag` 常量的值为纯文本标签的小写版,`reStackedTag` 常量稍微复杂一些,它的值是一个正则表达式实例,并且使用 `reCache[stackedTag]` 做了缓存,我们看一下 `reStackedTag` 正则的作用是什么,如下:
|
|
|
|
|
|
```js
|
|
|
new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
|
|
@@ -1743,7 +1743,7 @@ html = rest
|
|
|
parseEndTag(stackedTag, index - endTagLength, index)
|
|
|
```
|
|
|
|
|
|
-上面的代码中,首先跟新 `index` 的值,用 `html` 原始字符串的值减去 `rest` 字符串的长度,我们知道 `rest` 常量保存着剩余的字符串,所以二者的差就是被替换掉的那部分字符串的字符数。接着将 `rest` 常量的值赋值给 `html`,所以如果有剩余的字符串的话,它们将在下一次 `while` 循环被处理,最后调用 `parseEndTag` 函数解析纯文本标签的结束标签,这样就大功告成了。
|
|
|
+上面的代码中,首先更新 `index` 的值,用 `html` 原始字符串的值减去 `rest` 字符串的长度,我们知道 `rest` 常量保存着剩余的字符串,所以二者的差就是被替换掉的那部分字符串的字符数。接着将 `rest` 常量的值赋值给 `html`,所以如果有剩余的字符串的话,它们将在下一次 `while` 循环被处理,最后调用 `parseEndTag` 函数解析纯文本标签的结束标签,这样就大功告成了。
|
|
|
|
|
|
可以发现对于纯文本标签的处理宗旨就是将其内容作为纯文本对待。
|
|
|
|