Skip to content

Commit

Permalink
Vue模板编译原理
Browse files Browse the repository at this point in the history
  • Loading branch information
yzsunlei committed Dec 18, 2019
1 parent fc18b05 commit ee1be93
Showing 1 changed file with 87 additions and 70 deletions.
157 changes: 87 additions & 70 deletions 1.4.Vue模板编译原理-Template生成AST.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
## 编译过程
* 模板编译是Vue中比较核心的一部分。关于 Vue 编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,前后关系如下:
模板编译是Vue中比较核心的一部分。关于Vue编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,前后关系如下:

> 第一步:将模板字符串转换成element ASTs(解析器)
> 第二步:对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
> 第三步:使用element ASTs生成render函数代码字符串(代码生成器)
* 对应的Vue源码如下,源码位置在`src/compiler/index.js`
对应的Vue源码如下,源码位置在`src/compiler/index.js`

```vuejs
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
Expand All @@ -29,18 +30,21 @@ export const createCompiler = createCompilerCreator(function baseCompile (
})
```

* 这篇文档主要讲第一步将模板字符串转换成对象语法树(element ASTs),对应的源码实现我们通常称之为解析器。
这篇文档主要讲第一步将模板字符串转换成对象语法树(element ASTs),对应的源码实现我们通常称之为解析器。

## 解析器运行过程
* 在分析解析器的原理前,我们先举例看下解析器的具体作用。
在分析解析器的原理前,我们先举例看下解析器的具体作用。

来一个最简单的实例:

* 例如:
```html
<div>
<p>{{name}}</p>
</div>
```
* 上面的代码是一个比较简单的模板,它转换成AST后的样子如下:

上面的代码是一个比较简单的模板,它转换成AST后的样子如下:

```javascript
{
tag: "div"
Expand Down Expand Up @@ -71,14 +75,13 @@ export const createCompiler = createCompilerCreator(function baseCompile (
]
}
```
* 其实AST并不是什么很神奇的东西,不要被它的名字吓倒。它只是用JS中的对象来描述一个节点,一个对象代表一个节点,
对象中的属性用来保存节点所需的各种数据。

* 事实上,解析器内部也分了好几个子解析器,比如HTML解析器、文本解析器以及过滤器解析器,其中最主要的是HTML解析器。
顾名思义,HTML解析器的作用是解析HTML,它在解析HTML的过程中会不断触发各种钩子函数。这些钩子函数包括开始标签钩子函数、
结束标签钩子函数、文本钩子函数以及注释钩子函数。
其实AST并不是什么很神奇的东西,不要被它的名字吓倒。它只是用JS中的对象来描述一个节点,一个对象代表一个节点,对象中的属性用来保存节点所需的各种数据。

事实上,解析器内部也分了好几个子解析器,比如HTML解析器、文本解析器以及过滤器解析器,其中最主要的是HTML解析器。顾名思义,HTML解析器的作用是解析HTML,它在解析HTML的过程中会不断触发各种钩子函数。这些钩子函数包括开始标签钩子函数、结束标签钩子函数、文本钩子函数以及注释钩子函数。

我们先看下解析器整体的代码结构,源码位置`src/compiler/parser/index.js`

* 我们先看下解析器整体的代码结构,源码位置`src/compiler/parser/index.js`
```vuejs
parseHTML(template, {
warn,
Expand Down Expand Up @@ -108,26 +111,23 @@ parseHTML(template, {
})
```

* 实际上,模板解析的过程就是不断调用钩子函数的处理过程。整个过程,读取template字符串,使用不同的正则表达式,匹配到不同的
内容,然后触发对应不同的钩子函数处理匹配到的截取片段,比如开始标签正则匹配到开始标签,触发start钩子函数,钩子函数处理匹配到的
开始标签片段,生成一个标签节点添加到抽象语法树上。
实际上,模板解析的过程就是不断调用钩子函数的处理过程。整个过程,读取template字符串,使用不同的正则表达式,匹配到不同的内容,然后触发对应不同的钩子函数处理匹配到的截取片段,比如开始标签正则匹配到开始标签,触发start钩子函数,钩子函数处理匹配到的开始标签片段,生成一个标签节点添加到抽象语法树上。

还举上面那个例子来说:

* 举一下上面那个例子:
```vuejs
<div>
<p>{{name}}</p>
</div>
```

* 整个解析运行过程就是:解析到<div>时,会触发一个标签开始的钩子函数start,处理匹配片段,生成一个标签节点添加到AST上;
然后解析到<p>时,又触发一次钩子函数start,处理匹配片段,又生成一个标签节点并作为上一个节点的子节点添加到AST上;
接着解析到{{name}}这行文本,此时触发了文本钩子函数chars,处理匹配片段,生成一个带变量文本(变量文本下面会讲到)标签节点并作为上一个节点的子节点添加到AST上;
然后解析到</p>,触发了标签结束的钩子函数end;接着继续解析到</div>,此时又触发一次标签结束的钩子函数end,解析结束。
整个解析运行过程就是:解析到<div>时,会触发一个标签开始的钩子函数start,处理匹配片段,生成一个标签节点添加到AST上;然后解析到<p>时,又触发一次钩子函数start,处理匹配片段,又生成一个标签节点并作为上一个节点的子节点添加到AST上;接着解析到{{name}}这行文本,此时触发了文本钩子函数chars,处理匹配片段,生成一个带变量文本(变量文本下面会讲到)标签节点并作为上一个节点的子节点添加到AST上;然后解析到</p>,触发了标签结束的钩子函数end;接着继续解析到</div>,此时又触发一次标签结束的钩子函数end,解析结束。

## 正则匹配
* 模板解析过程会涉及到许许多多的正则匹配,知道每个正则有什么用途,会更加方便之后的分析。
模板解析过程会涉及到许许多多的正则匹配,知道每个正则有什么用途,会更加方便之后的分析。

那我们先来看看这些正则表达式,源码位置在`src/compiler/parser/index.js`

* 那我们先来看看这些正则表达式,源码位置在`src/compiler/parser/index.js`
```javascript
export const onRE = /^@|^v-on:/
export const dirRE = process.env.VBIND_PROP_SHORTHAND
Expand All @@ -150,10 +150,11 @@ const whitespaceRE = /\s+/g

const invalidAttributeRE = /[\s"'<>\/=]/
```
* 上面这些正则相对来说比较简单,基本上都是用来匹配Vue中自定义的一些语法格式,如onRE匹配 @ 或 v-on 开头的属性,
forAliasRE匹配v-for中的属性值,比如item in items、(item, index) of items。

* 下面这些就是专门针对html的一些正则匹配,源码位置在`src/compiler/parser/html-parser.js`
上面这些正则相对来说比较简单,基本上都是用来匹配Vue中自定义的一些语法格式,如onRE匹配 @ 或 v-on 开头的属性,forAliasRE匹配v-for中的属性值,比如item in items、(item, index) of items。

下面这些就是专门针对html的一些正则匹配,源码位置在`src/compiler/parser/html-parser.js`

```javascript
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
Expand All @@ -166,16 +167,16 @@ const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
```
* 这些正则表达式相对来说就复杂一些,如attribute用来匹配标签的属性,startTagOpen、startTagClose用于匹配标签的开始、结束部分等。
这些正则表达式的写法就不多说了,有兴趣的朋友可以针对这些正则一个一个的去测试一下。

这些正则表达式相对来说就复杂一些,如attribute用来匹配标签的属性,startTagOpen、startTagClose用于匹配标签的开始、结束部分等。这些正则表达式的写法就不多说了,有兴趣的朋友可以针对这些正则一个一个的去测试一下。

## HTML解析器
* 这里我们来看看HTMl解析器。
这里我们来看看HTMl解析器。

事实上,解析HTML模板的过程就是循环的过程,简单来说就是用HTML模板字符串来循环,每轮循环都从HTML模板中截取一小段字符串,然后重复以上过程,直到HTML模板被截成一个空字符串时结束循环,解析完毕。

* 事实上,解析HTML模板的过程就是循环的过程,简单来说就是用HTML模板字符串来循环,
每轮循环都从HTML模板中截取一小段字符串,然后重复以上过程,直到HTML模板被截成一个空字符串时结束循环,解析完毕。
我们通过源码,就可以看到整个函数逻辑就是被一个while循环包裹着。源码位置在:`src/compiler/parser/html-parser.js`

* 我们通过源码,就可以看到整个函数逻辑就是被一个while循环包裹着。源码位置在:`src/compiler/parser/html-parser.js`
```javascript
export function parseHTML (html, options) {
const stack = []
Expand All @@ -194,91 +195,108 @@ export function parseHTML (html, options) {
}
```

* 下面我用一个简单的模板,模拟一下HTML解析的过程,以便于更好的理解。
下面我用一个简单的模板,模拟一下HTML解析的过程,以便于更好的理解。

```vuejs
<div>
<p>{{text}}</p>
</div>
```

* 最初的HTML模板:
最初的HTML模板:

```vuejs
<div>
<p>{{text}}</p>
</div>
```

* 第一轮循环时,截取出一段字符串<div>,解析出是div开始标签并且触发钩子函数start,截取后的结果为:
第一轮循环时,截取出一段字符串<div>,解析出是div开始标签并且触发钩子函数start,截取后的结果为:

```vuejs
<p>{{text}}</p>
</div>
```

* 第二轮循环时,截取出一段换行空字符串,会触发钩子函数chars,截取后的结果为:
第二轮循环时,截取出一段换行空字符串,会触发钩子函数chars,截取后的结果为:

```vuejs
<p>{{text}}</p>
</div>
```

* 第三轮循环时,截取出一段字符串<p>,解析出是p开始标签并且触发钩子函数start,截取后的结果为:
第三轮循环时,截取出一段字符串<p>,解析出是p开始标签并且触发钩子函数start,截取后的结果为:

```vuejs
{{text}}</p>
</div>
```

* 第四轮循环时,截取出一段字符串{{name}},解析出是变量字符串并且触发钩子函数chars,截取后的结果为:
第四轮循环时,截取出一段字符串{{name}},解析出是变量字符串并且触发钩子函数chars,截取后的结果为:

```vuejs
</p>
</div>
```

* 第五轮循环时,截取出一段字符串</p>,解析出是p闭合标签并且触发钩子函数end,截取后的结果为:
第五轮循环时,截取出一段字符串</p>,解析出是p闭合标签并且触发钩子函数end,截取后的结果为:

```vuejs
</div>
```

* 第六轮循环时,截取出一段换行空字符串,会触发钩子函数chars,截取后的结果为:
第六轮循环时,截取出一段换行空字符串,会触发钩子函数chars,截取后的结果为:

```vuejs
</div>
```

* 第七轮循环时,截取出一段字符串</div>,解析出是div闭合标签并且触发钩子函数end,截取后的结果为:
第七轮循环时,截取出一段字符串</div>,解析出是div闭合标签并且触发钩子函数end,截取后的结果为:

```vuejs
```

* 第八轮循环时,发现只有一个空字符串,解析完毕,循环结束。
第八轮循环时,发现只有一个空字符串,解析完毕,循环结束。

现在,是不是就对HTML解析过程很清楚了。其实循环过程对每次匹配到的片段进行分析记录还是很复杂的,因为被截取的片段分很多种类型,比如:

* 现在,是不是就对HTML解析过程很清楚了。其实循环过程对每次匹配到的片段进行分析记录还是很复杂的,因为被截取的片段分很多种类型,比如:
> 开始标签,例如<div>
> 结束标签,例如</div>
> HTML注释,例如<!-- 注释 -->
> DOCTYPE,例如<!DOCTYPE html>
> 条件注释,例如<!--[if !IE]>-->注释<!--<![endif]-->
> 文本,例如'字符串'。
* 对每个片段的具体处理这里就不说了,有兴趣的直接看源码去。
> 条件注释,例如<!--[if !IE]>-->注释<!--<![endif]-->
> 文本,例如'字符串'
对每个片段的具体处理这里就不说了,有兴趣的直接看源码去。

## 文本解析器
* 文本解析器是对HTML解析器解析出来的文本进行二次加工。文本其实分两种类型,一种是纯文本,另一种是带变量的文本。如下:
文本解析器是对HTML解析器解析出来的文本进行二次加工。文本其实分两种类型,一种是纯文本,另一种是带变量的文本。如下:

这种就是纯文本:

* 这种就是纯文本:
```vuejs
这里有段文本
```

* 这种就是带变量的文本:
这种就是带变量的文本:

```vuejs
文本内容:{{text}}
```

* 上面HTML解析器在解析文本时,并不会区分文本是否是带变量的文本。如果是纯文本,不需要进行任何处理;
但如果是带变量的文本,那么需要使用文本解析器进一步解析。因为带变量的文本在使用虚拟DOM进行渲染时,需要将变量替换成变量中的值。
上面HTML解析器在解析文本时,并不会区分文本是否是带变量的文本。如果是纯文本,不需要进行任何处理;但如果是带变量的文本,那么需要使用文本解析器进一步解析。因为带变量的文本在使用虚拟DOM进行渲染时,需要将变量替换成变量中的值。

我们知道,HTML解析器在碰到文本时,会触发chars钩子函数,我们先来看看钩子函数里面是怎么区分普通文本和变量文本的。

* 我们知道,HTML解析器在碰到文本时,会触发chars钩子函数,我们先来看看钩子函数里面是怎么区分普通文本和变量文本的。
源码位置在:`src/compiler/parser/html-parser.js`

* 源码位置在:`src/compiler/parser/html-parser.js`
```vuejs
chars (text: string, start: number, end: number) {
//...
Expand All @@ -301,9 +319,10 @@ chars (text: string, start: number, end: number) {
}
```

* 我们重点看`res = parseText(text, delimiters)`这一行,通过条件判断设置不同的类型。事实上type=2表示表达式类型,type=3表示普通文本类型。
我们重点看`res = parseText(text, delimiters)`这一行,通过条件判断设置不同的类型。事实上type=2表示表达式类型,type=3表示普通文本类型。

我们再来看看parseText函数具体做了什么

* 我们再来看看parseText函数具体做了什么
```vuejs
export function parseText (
text: string,
Expand Down Expand Up @@ -348,11 +367,10 @@ export function parseText (
}
```

* 实际上这个函数就是处理带变量的文本,首先如果是纯文本,直接return。如果是带变量的文本,
使用正则表达式匹配出文本中的变量,先把变量左边的文本添加到数组中,然后把变量改成_s(x)这样的形式也添加到数组中。
如果变量后面还有变量,则重复以上动作,直到所有变量都添加到数组中。如果最后一个变量的后面有文本,就将它添加到数组中。
实际上这个函数就是处理带变量的文本,首先如果是纯文本,直接return。如果是带变量的文本,使用正则表达式匹配出文本中的变量,先把变量左边的文本添加到数组中,然后把变量改成_s(x)这样的形式也添加到数组中。如果变量后面还有变量,则重复以上动作,直到所有变量都添加到数组中。如果最后一个变量的后面有文本,就将它添加到数组中。

那么对于上面示例处理结果如下:

* 那么对于上面示例处理结果如下:
```vuejs
parseText('这里有段文本')
// undefined
Expand All @@ -361,19 +379,18 @@ parseText('文本内容:{{text}}')
// '"文本内容:" + _s(text)'
```

* 好了,对于文本解析器就这么多内容。
好了,对于文本解析器就这么多内容。

## 总结一下
* 模板解析是Vue模板编译的第一步,即通过模板得到AST(抽象语法树)。
模板解析是Vue模板编译的第一步,即通过模板得到AST(抽象语法树)。

* 生成AST的过程核心就是借助HTML解析器,当HTML解析器通过正则匹配到不同的片段时会触发对应不同的钩子函数,通过钩子函数对匹配片段
进行解析我们可以构建出不同的节点。
生成AST的过程核心就是借助HTML解析器,当HTML解析器通过正则匹配到不同的片段时会触发对应不同的钩子函数,通过钩子函数对匹配片段进行解析我们可以构建出不同的节点。

* 文本解析器是对HTML解析器解析出来的文本进行二次加工,主要是为了处理带变量的文本。
文本解析器是对HTML解析器解析出来的文本进行二次加工,主要是为了处理带变量的文本。

## 相关资料
* [https://juejin.im/post/5ca44160518825440a4b9fab](https://juejin.im/post/5ca44160518825440a4b9fab)
* [https://segmentfault.com/a/1190000012922342](https://segmentfault.com/a/1190000012922342)
* [https://www.jianshu.com/p/743166a8968c](https://www.jianshu.com/p/743166a8968c)
* [https://segmentfault.com/a/1190000013763590](https://segmentfault.com/a/1190000013763590)
* [https://github.com/liutao/vue2.0-source/blob/master/compile%E2%80%94%E2%80%94%E7%94%9F%E6%88%90ast.md](https://github.com/liutao/vue2.0-source/blob/master/compile%E2%80%94%E2%80%94%E7%94%9F%E6%88%90ast.md)
## 相关
- [https://juejin.im/post/5ca44160518825440a4b9fab](https://juejin.im/post/5ca44160518825440a4b9fab)
- [https://segmentfault.com/a/1190000012922342](https://segmentfault.com/a/1190000012922342)
- [https://www.jianshu.com/p/743166a8968c](https://www.jianshu.com/p/743166a8968c)
- [https://segmentfault.com/a/1190000013763590](https://segmentfault.com/a/1190000013763590)
- [https://github.com/liutao/vue2.0-source/blob/master/compile%E2%80%94%E2%80%94%E7%94%9F%E6%88%90ast.md](https://github.com/liutao/vue2.0-source/blob/master/compile%E2%80%94%E2%80%94%E7%94%9F%E6%88%90ast.md)

0 comments on commit ee1be93

Please sign in to comment.