咱们在Vue中会使用一些变量,表达式,指令来填充模板,可是这些语法在HTML中是不存在的,那么Vue是如何对这样的模板进行编译的呢?javascript
模板编译的主要做用是将Vue模板编译为渲染函数,首先将模板解析成AST(抽象语法树),而后使用AST生成渲染函数。html
首先咱们要知道Vue每次渲染,都会生成一份新的vNode与旧的vNode进行对比,在生成渲染函数以前还会遍历一遍AST,为全部的静态节点作一个编辑,在从新渲染时,不会生成新得节点,而是直接克隆已存在的以前的静态节点。java
因此整体过程是:将模板解析成AST=>遍历AST标记静态节点=>使用AST生成渲染函数
。 正则表达式
在这一步骤中,须要通过解析器将模板解析AST,而后还须要通过优化器,遍历AST找出静态节点并标记。算法
在解析器内部还分红了文本解析器,HTML解析器和过滤器解析器。express
其中核心部分是HTML解析器,做用是用来解析字符串模板。变量解析器用于解析带有模板的文本变量,而不带用变量的文本节点就是刚才所说的静态节点,不须要解析。过滤器解析器用来解析过滤器。解析结果AST是一种以节点为结构的树形结构的对象,一个对象表示一个节点,对象的属性用来保存节点所须要的数据。数组
解析模板例如:函数
<div>
<p>{{name}}</p>
</div>
复制代码
解析成AST以后:post
//里面的内容后续会解释
{
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)"
}]
}
]
}
复制代码
解析器在解析HTML的过程当中会不断触发各类钩子函数。这些钩子函数包括开始标签钩子函数、结束标签钩子函数、文本钩子函数以及注释钩子函数。性能
例如:
parseHTML(template, {
start (tag, attrs, unary) {
// 每当解析到标签的开始位置时,触发该函数
},
end () {
// 每当解析到标签的结束位置时,触发该函数
},
chars (text) {
// 每当解析到文本时,触发该函数
},
comment (text) {
// 每当解析到注释时,触发该函数
}
})
复制代码
咱们简单举一个例子来讲明上述方法是如何构建AST节点的:
<div><p>我是一个节点</p></div>
复制代码
首先,解析器会将html模板做为一段字符串模板从前向后进行解析,解析到<div>
时,会触发一个标签开始的钩子函数start();而后解析到<p>
时,又触发一次钩子函数start();接着解析到我是一个节点
这行文本,此时触发了文本钩子函数chars();而后解析到</p>
,触发了标签结束的钩子函数end();接着继续解析到</div>
,此时又触发一次标签结束的钩子函数end(),解析结束。
start()函数你能够看做为HTML解析函数,他的三个参数分别是分别是tag、attrs和unary,分别表明标签名、标签的属性以及是不是自闭合标签。
而文本节点的解析函数chars和注释节点的解析函数comment都只有一个参数text。这是由于构建元素节点须要知道标签名、属性和是不是自闭合元素,而构建注释节点和文本节点时只须要知道文本内容便可。 咱们将上面的parseHTML()扩充一下:
//咱们模拟一个建立AST元素类型节点的函数
function createASTElement (tag, attrs, parent) {
// 返回的是一个节点对象
return {
type: 1, // 指定节点类型 1.元素节点
tag, // 指定节点
attrsList: attrs, // 指定节点属性
parent, // 指定是不是自闭合标签
children: []
}
}
parseHTML(template, {
start (tag, attrs, unary) {
// 每当解析到标签的开始位置时,触发该函数
// 将标签名、标签的属性以及是不是自闭合标签传入
let element = createASTElement(tag, attrs, currentParent)
},
end () {
// 每当解析到标签的结束位置时,触发该函数
},
chars (text) {
// 每当解析到文本时,触发该函数
// 返回的是一个文本节点对象
// 文本分两种类型 2.带变量的动态文本节点 3.不带变量的纯文本节点
let element = {type: 3, text}
},
comment (text) {
// 每当解析到注释时,触发该函数
// 返回的是一个注释节点对象,注释文本和文本的区别是打上了isComment标记
let element = {type: 3, text, isComment: true}
}
})
复制代码
可是使用上述方式建立的节点虽然带有节点对象信息,可是是扁平的,没有层级关系,而Vue使用了出入栈的方式来构建一个AST结构对象,为以前的扁平数据实现层级关系。
每次解析HTML,都会使用一个栈来存储维护,当触发start()函数时,将当前构建的节点推入栈中;每当触发钩子函数end()时,就从栈中弹出上一个节点。举个例子:
<div>
<h1>我是h1</h1>
<p>我是文本</p>
</div>
复制代码
并将模板字符串中的div开始标签从模板中截取掉
。
将h1添加到div的子节点中(也就是children中)
,而且将h1节点推入栈中,同时从模板中将h1的开始标签截取掉。
其中对开始标签,结束标签,还有标签属性的解析基本是使用了大量正则表达式:去解析<div
</div>
:
class=
这样的字符串,去断定这是一个什么标签该去触发什么函数,不作过多描述。
这个过程如何解析HTML中的注释,条件注释,DOCTYPE,文本?
HTML中的注释,判断<!--
,经过indexOf找到注释结束位置-->
的下标,而后将结束位置前的字符都截取掉。条件注释注释用提早的表达式判断<
,条件注释会被直接截取掉。DOCTYPE直接匹配这段字符,根据它的length属性来决定要截取多长的字符串。文本咱们只须要找到>
与下一个<
在什么位置,这以前的全部字符都属于文本。
节点不完整?
<div><p></div>
复制代码
在上面的代码中,p标签没有结束标签,那么当HTML解析器解析到div的结束标签时,发现栈内元素倒是p标签。就会从栈顶向栈底遍历寻找到div标签,在找到div标签以前遇到的全部其余标签都会标记为忘记闭合的标签,在非生产环境下在控制台打印警告提示。
为何文本解析器要单独说,由于文本其实分两种类型,一种是纯文本,另外一种是带变量的文本。
Hello name
Hello {{name}}
复制代码
若是是纯文本,不须要进行任何处理;但若是是带变量的文本,那么须要使用文本解析器进一步解析。由于带变量的文本在使用虚拟DOM进行渲染时,须要将变量替换成变量中的值。
静态节点:
<p>我就是一个纯文本的静态节点</p>
复制代码
优化器则是将解析完的AST进行遍历,找出静态节点并标记,在下次更新对比虚拟DOM的vNode时,若是发现这两个节点是静态节点,则直接跳过更新节点的流程。达到进一步避免一些无用的DOM操做来提高性能,由于静态节点在首次渲染后必定不会改变。
代码生成器是将解析完的AST转化为渲染函数须要的内容,这个内容叫代码字符串,例如:
<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生成代码字符串
`with(this) {return _c('div', [_c('p', [_v(_s(name))]), _v(" "), _m(0)])}`
复制代码
以后将这串代码字符串传到Vue的渲染函数中,渲染函数根据参数结构,调用相关的建立vNode的方法(生成后的代码字符串中看到了有几个函数调用 _c,_v,_s,这是Vue内部的一些渲染函数,_c能够建立元素类型的vNode,_v能够建立文本类型的vNode,_e能够建立注释类型的vNode)最后组成一份虚拟DOM结构。
咱们拿_c来解释一下这个字符串的结构:
<p title="biaoti">name</p>
复制代码
with(this){
return _c(
'p', // 标签名
{
attrs:{"title":"biaoti"},
}, // 属性
[_v("name")] // 子节点
)
}
复制代码
代码生成器的整体逻辑其实就是递归ATS,而后根据ATS结构拼出这样的_c('div',[_c('p',[_v(_s(name))])])
字符串,再将其传入渲染函数执行。
至于具体的AST转换过程就不作深刻解释,会令文章显得枯燥。
咱们以上简单讲述了Vue对模板编译的总体流程:解析器(模板字符串转换成AST),优化器(标记静态节点)和代码生成器(将AST装换成带结构的代码字符串)。
解析器经过使用一个栈来维护节点,每从模板字符串中截取一个节点字符串,就将其推入栈中,同时构建一个AST节点,一直到结束节点在将其推出栈,如此循环最后构建出一套带有结构的AST对象。
优化器是经过遍历AST节点,对其中的静态节点作标记,同时最后标记处根静态节点,节省部分没必要要的性能消耗。
代码生成器也是经过遍历去拼出一个渲染函数执行的代码字符串,遍历的过程根据不一样的节点类型type调用不一样的生成字符串方法,最后拼出一个完整的 render 函数须要的代码字符串。
后续还有两篇:
《Vue不看源码懂原理》系列——Vue的diff算法不难懂(直接传送)
《Vue不看源码懂原理》系列——Vue的实例函数和指令解密(下周)
之有一篇用心总结的《Javascript垃圾回收原理》没太有响应,我以为你们能够看一看,耐心一下的话比较好理解。
点个赞,我加油
点关注,不迷路,哈哈哈