在前面的学习中,我们知道,Vue的模板编译,分为解析器、优化器、代码生成器三个模块依次来实现。今天,我们先来深入了解一下解析器。
解析器,就是把<template></template>模板,根据一定的解析规则解析出有效的信息,最后用这些信息形成AST(保存了节点所需的各种数据的一个JS对象)。
在模板内,除了有常规的HTML标签外,还会有一些文本信息以及在文本信息中包含的过滤器。这些不同的内容在解析起来需要不同的解析规则,所以解析器不只一个,除了有解析常规HTML的HTML解析器,还有解析文本的文本解析器以及解析过滤器的过滤器解析器。
运行流程
既然解析器有多个,那么它们是怎么串联运行的?
因为文本信息和过滤器存在于HTML标签之内,所以先用HTML解析器解析整个模板,在解析过程中如果碰到文本内容,那就调用文本解析器来解析文本,如果碰到文本中包含过滤器那就调用过滤器解析器来解析。
具体到Vue代码中,parse 函数就是解析器的主函数,在parse 函数内调用了parseHTML 函数对模板字符串进行解析,在parseHTML 函数解析模板字符串的过程中,如果遇到文本信息,就会调用文本解析器parseText函数进行文本解析;如果遇到文本中包含过滤器,就会调用过滤器解析器parseFilters函数进行解析。
运行原理
事实上,解析HTML模板的过程就是循环的过程,简单来说,就是用HTML模板字符串来循环,每轮循环都从HTML模板中截取一小段字符串,然后重复以上过程,直到HTML模板被截成一个空字符串时结束循环,解析完毕。
在截取一小段字符串时,有可能截取到开始标签,也有可能截取到结束标签,又或者是文本或注释,我们可以根据截取的字符串的类型来触发不同的钩子函数。这些钩子函数包括开始标签钩子函数、结束标签钩子函数、文本钩子函数以及注释钩子函数。
export function parse(template, options) {
// ...
parseHTML(template, {
start (tag, attrs, unary) {
//每当解析到标签的开始位置时,触发该函数
},
end () {
//每当解析到标签的结束位置时,触发该函数
},
chars (text: string) {
// 每当解析到文本时,触发该函数
},
comment (text: string) {
// 每当解析到注释时,触发该函数
}
})
return root
}
举个例子:
<div>
<p>
hello world
</p>
</div>
当上面这个模板被HTML解析器解析时,所触发的钩子函数依次是:start、start、chars、end和end。也就是说,解析器其实是从前向后解析的。
因此,我们可以在钩子函数中构建AST节点。在start钩子函数中构建元素类型的节点,在chars钩子函数中构建文本类型的节点,在comment钩子函数中构建注释类型的节点。
当解析器不再触发钩子函数时,就说明所有模板都解析完毕,所有类型的节点都在钩子函数中构建完成,即AST构建完成。
构建AST节点层级关系
我们知道了解析器是如何解析各种不同类型的内容并且调用钩子函数创建不同类型的AST节点。但是上面创建的AST节点都是单独创建且分散的,而真正的DOM节点都是有层级关系的,那如何来保证AST节点的层级关系与真正的DOM节点相同呢?
Vue在HTML解析器的开头定义了一个栈stack,这个栈的作用就是用来维护AST节点层级的。
通过前文我们知道,HTML解析器在从前向后解析模板字符串时,每当遇到开始标签时就会调用start钩子函数,那么在start钩子函数内部我们可以将解析得到的开始标签推入栈中,而每当遇到结束标签时就会调用end钩子函数,那么我们也可以在end钩子函数内部将解析得到的结束标签所对应的开始标签从栈中弹出。
这样就保证了每当触发钩子函数start时,栈的最后一个节点就是当前正在构建的节点的父节点。
举个例子:
<div>
<p>
<span></span>
</p>
</div>
当解析到开始标签<div>时,就把div推入栈中,然后继续解析,当解析到<p>时,再把p推入栈中,同理,再把span推入栈中,当解析到结束标签</span>时,此时栈顶的标签刚好是span的开始标签,那么就用span的开始标签和结束标签构建AST节点,并且从栈中把span的开始标签弹出,那么此时栈中的栈顶标签p就是构建好的span的AST节点的父节点,如下图:
文本解析
实际上,HTML解析器解析出来的文本,需要文本解析器来进行二次加工。为什么这么说呢?
文本其实分两种类型,一种是纯文本,另一种是带变量的文本。例如下面这样的文本是纯文本:
hello world
而下面这样的是带变量的文本:
hello {{name}}
在Vue模板中,我们可以使用变量来填充模板。而HTML解析器在解析文本时,并不会区分文本是否是带变量的文本。如果是纯文本,不需要进行任何处理,但如果是带变量的文本,那么需要使用文本解析器进一步解析。因为带变量的文本在使用虚拟DOM进行渲染时,需要将变量替换成变量中的值。
过滤器解析
vue的filter允许用在两个地方,一个是双括号插值,一个是v-bind表达式后面,如果解析到这两种情况,则执行parseFilters来解析filter。
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
小结
解析器的作用是通过模板得到AST(抽象语法树)。
生成AST的过程需要借助HTML解析器,当HTML解析器触发不同的钩子函数时,我们可以构建出不同的节点。
我们可以通过栈来得到当前正在构建的节点的父节点,然后将构建出的节点添加到父节点的下面。
最终,当HTML解析器运行完毕后,我们可以得到一个完整的带DOM层级关系的AST。
HTML解析器的内部原理是一小段一小段地截取模板字符串,每截取一小段字符串,就会根据截出来的字符串类型触发不同的钩子函数,直到模板字符串截空停止运行。