本文我们一起通过学习Vue模板编译原理(一)-Template生成AST来分析Vue源码。预计接下来会围绕Vue源码来整理一些文章,如下。

这些文章统一放在我的git仓库:https://github.com/yzsunlei/javascript-series-code-analyzing。觉得有用记得star收藏。

编译过程

模板编译是Vue中比较核心的一部分。关于Vue编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,前后关系如下:

第一步:将模板字符串转换成element ASTs(解析器)

第二步:对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)

第三步:使用element ASTs生成render函数代码字符串(代码生成器)

对应的Vue源码如下,源码位置在src/compiler/index.js

export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 1.parse,模板字符串 转换成 抽象语法树(AST)
const ast = parse(template.trim(), options)
// 2.optimize,对 AST 进行静态节点标记
if (options.optimize !== false) {
optimize(ast, options)
}
// 3.generate,抽象语法树(AST) 生成 render函数代码字符串
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})

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

解析器运行过程

在分析解析器的原理前,我们先举例看下解析器的具体作用。

来一个最简单的实例:

<div>
<p>{{name}}</p>
</div>

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

{
tag: "div"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: undefined,
attrsList: [],
attrsMap: {},
children: [
{
tag: "p"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: {tag: "div", ...},
attrsList: [],
attrsMap: {},
children: [{
type: 2,
text: "{{name}}",
static: false,
expression: "_s(name)"
}]
}
]
}

其实AST并不是什么很神奇的东西,不要被它的名字吓倒。它只是用JS中的对象来描述一个节点,一个对象代表一个节点,对象中的属性用来保存节点所需的各种数据。

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

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

parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
// 每当解析到标签的开始位置时,触发该函数
start (tag, attrs, unary, start, end) {
//...
},
// 每当解析到标签的结束位置时,触发该函数
end (tag, start, end) {
//...
},
// 每当解析到文本时,触发该函数
chars (text: string, start: number, end: number) {
//...
},
// 每当解析到注释时,触发该函数
comment (text: string, start, end) {
//...
}
})

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

还举上面那个例子来说:

<div>
<p>{{name}}</p>
</div>

整个解析运行过程就是:解析到

时,会触发一个标签开始的钩子函数start,处理匹配片段,生成一个标签节点添加到AST上;然后解析到

时,又触发一次钩子函数start,处理匹配片段,又生成一个标签节点并作为上一个节点的子节点添加到AST上;接着解析到{{name}}这行文本,此时触发了文本钩子函数chars,处理匹配片段,生成一个带变量文本(变量文本下面会讲到)标签节点并作为上一个节点的子节点添加到AST上;然后解析到

,触发了标签结束的钩子函数end;接着继续解析到

,此时又触发一次标签结束的钩子函数end,解析结束。

正则匹配

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

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

export const onRE = /^@|^v-on:/
export const dirRE = process.env.VBIND_PROP_SHORTHAND
? /^v-|^@|^:|^\.|^#/
: /^v-|^@|^:|^#/
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
const dynamicArgRE = /^\[.*\]$/ const argRE = /:(.*)$/
export const bindRE = /^:|^\.|^v-bind:/
const propBindRE = /^\./
const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g const slotRE = /^v-slot(:|$)|^#/ const lineBreakRE = /[\r\n]/
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

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
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 = /^<!\[/

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

HTML解析器

这里我们来看看HTMl解析器。

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

我们通过源码,就可以看到整个函数逻辑就是被一个while循环包裹着。源码位置在:src/compiler/parser/html-parser.js

export function parseHTML (html, options) {
const stack = []
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
let index = 0
let last, lastTag
while (html) {
//...
} parseEndTag() //...
}

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

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

最初的HTML模板:

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

第一轮循环时,截取出一段字符串

,解析出是div开始标签并且触发钩子函数start,截取后的结果为:


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

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

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

第三轮循环时,截取出一段字符串

,解析出是p开始标签并且触发钩子函数start,截取后的结果为:

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

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

  </p>
</div>

第五轮循环时,截取出一段字符串

,解析出是p闭合标签并且触发钩子函数end,截取后的结果为:


</div>

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

</div>

第七轮循环时,截取出一段字符串

,解析出是div闭合标签并且触发钩子函数end,截取后的结果为:

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

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

开始标签,例如<div>

结束标签,例如</div>

HTML注释,例如<!-- 注释 -->

DOCTYPE,例如<!DOCTYPE html>

条件注释,例如<!--[if !IE]>-->注释<!--<![endif]-->

文本,例如'字符串'

对每个片段的具体处理这里就不说了,有兴趣的直接看源码去。

文本解析器

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

这种就是纯文本:

这里有段文本

这种就是带变量的文本:

文本内容:{{text}}

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

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

源码位置在:src/compiler/parser/html-parser.js

chars (text: string, start: number, end: number) {
//...
let child: ?ASTNode
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
child = {
type: 3,
text
}
}
//...
children.push(child)
}

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

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

export function parseText (
text: string,
delimiters?: [string, string]
): TextParseResult | void {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
// 匹配不到带变量时直接返回了
if (!tagRE.test(text)) {
return
}
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
// 对匹配到的变量循环处理成表达式
while ((match = tagRE.exec(text))) {
index = match.index
// push text token
// 先把 { { 前边的文本添加到tokens中
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index))
tokens.push(JSON.stringify(tokenValue))
}
// tag token
const exp = parseFilters(match[1].trim())
// 使用_s对变量进行包装
// 把变量改成`_s(x)`这样的形式也添加到数组中
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })
// 设置lastIndex来保证下一轮循环时,正则表达式不再重复匹配已经解析过的文本
lastIndex = index + match[0].length
}
// 当所有变量都处理完毕后,如果最后一个变量右边还有文本,就将文本添加到数组中
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex))
tokens.push(JSON.stringify(tokenValue))
}
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}

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

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

parseText('这里有段文本')
// undefined parseText('文本内容:{{text}}')
// '"文本内容:" + _s(text)'

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

总结一下

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

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

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

相关

[Vue源码]一起来学Vue模板编译原理(一)-Template生成AST的更多相关文章

  1. [Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串

    本文我们一起通过学习Vue模板编译原理(二)-AST生成Render字符串来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学V ...

  2. [Vue源码]一起来学Vue双向绑定原理-数据劫持和发布订阅

    有一段时间没有更新技术博文了,因为这段时间埋下头来看Vue源码了.本文我们一起通过学习双向绑定原理来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫 ...

  3. Vue源码分析(一) : new Vue() 做了什么

    Vue源码分析(一) : new Vue() 做了什么 author: @TiffanysBear 在了解new Vue做了什么之前,我们先对Vue源码做一些基础的了解,如果你已经对基础的源码目录设计 ...

  4. Vue 源码解读(3)—— 响应式原理

    前言 上一篇文章 Vue 源码解读(2)-- Vue 初始化过程 详细讲解了 Vue 的初始化过程,明白了 new Vue(options) 都做了什么,其中关于 数据响应式 的实现用一句话简单的带过 ...

  5. 【js】vue 2.5.1 源码学习 (十一) 模板编译compileToFunctions渲染函数

    大体思路(九) 本节内容: 1. compileToFunctions定位 1. compileToFunctions定位 ==> createCompiler = createCompiler ...

  6. Vue源码学习(零):内部原理解析

    本篇文章是在阅读<剖析 Vue.js 内部运行机制>小册子后总结所得,想要了解详细内容,请参考原文:https://juejin.im/book/5a36661851882538e2259 ...

  7. 从Vue源码中我学到了几点精妙方法

    话不多说,赶快试试这几个精妙方法吧!在工作中肯定会用得到. 立即执行函数 页面加载完成后只执行一次的设置函数. (function (a, b) { console.log(a, b); // 1,2 ...

  8. vue源码分析之new Vue过程

    实例化构造函数 从这里可以看出new Vue实际上是使vue构造函数实例化,然后调用_init方法 _init方法,该方法在 src/core/instance/init.js 中定义 Vue.pro ...

  9. Vue源码思维导图------------Vue选项的合并之$options

    本节将看下初始化中的$options: Vue.prototype._init = function (options?: Object) { const vm: Component = this / ...

随机推荐

  1. LeetCode97 Interleaving String

    Given s1, s2, s3, find whether s3 is formed by the interleaving of s1 and s2. (Hard) For example,Giv ...

  2. Javascript 用来验证电话号码的正则

    Javascript 用来验证电话号码的正则 在学习 Javascript 时学习到的. function telephoneCheck(str) { // return /^(1\s?)?(\d{3 ...

  3. Python中多线程与多进程的恩恩怨怨

    概念: 并发:当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运 ...

  4. python小数据池 is和 == 再谈编码

    1. 小数据池, id() 小数据池针对的是: int, str, bool 在py文件中几乎所有的字符串都会缓存. id() 查看变量的内存地址 2. is和==的区别 is 比较的是内存地址 == ...

  5. jmter对于函数的处理

    1.获取当天的时间函数 ${__time(yyyy-MM-dd_HH-mm-ss)} 2.获取随机函数 ${__Random(100,300,)} 3.查看结果数

  6. 异常处理之try catch finally

    package com.sxt.wrapper.test2; /* 0418 * 异常处理 * 采用异常处理的好处:保证程序发生异常后可以继续执行 * e.printStaceTrace:打印堆栈信息 ...

  7. Hbase架构与实现

  8. 禁用gpu首选

    import osos.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"os.environ["CUDA_VIS ...

  9. hdu 2410 Barbara Bennett's Wild Numbers

    Problem - 2410 挺好玩的一道题目.这道题的意思是给出一个模糊值以及一个确定值,要求求出模糊值中大于确定值的个数有多少. 这题我是直接用dfs的方法搜索的,对于每一位如果之前位置的形成的数 ...

  10. 通过Ajax提交form表单来提交上传文件

    Ajax 提交form方式可以将form表单序列化 然后将数据通过data提交至后台,例如: $.ajax({      url : "http://localhost:8080/" ...