深入浅出Vue.js(三) 模板编译
模板编译
在底层实现上,vue.js会将模板编译成虚拟DOM渲染函数。当应用内部的状态发生变化时,vue.js会结合响应式系统,找出最小数量的组件进行重新渲染以及最少量地进行DOM操作。
平时使用模板时,可以在模板中使用一些变量来填充模板,还可以在模板中使用JavaScript表达式,又或者使用一些指令等。这些功能在HTML语法中是不存在的,那么为什么在vue.js的模板中就可以使用很灵活的语法呢?这就多亏了模板编译赋予了模板强大的功能。
模板编译的主要目标就是生成渲染函数,而渲染函数的作用是每次执行它,它都会使用当前最新的状态生成一份新的vnode,然后使用这个vnode进行渲染。
将模板编译成渲染函数
将模板编译成渲染函数可以分为两个步骤:先将模板解析成AST(Abstract Syntax Tree,抽象语法树),然后再使用AST生成渲染函数。
AST,它只是用JavaScript中的对象来描述一个节点,一个对象表示一个节点,对象中的属性用来保存节点所需的各种数据。
注:由于静态节点不需要总是重新渲染,所以在生成AST之后,生成渲染函数之前这个阶段,需要做一个操作,那就是遍历一遍AST,给所有静态节点做一个标记,这样在虚拟DOM中更新节点时,如果发现节点有这个标记,就不会重新渲染。
- 将模板解析为AST
- 遍历AST标记静态节点
- 使用AST生成渲染函数
这三部分内容在模板编译中分别抽象出来三个模块来实现各自的功能,分别是:解析器、优化器、代码生成器。
解析器:解析器就是将模板解析成AST。在解析器内部,分成很多小解析器,其中包括过滤解析器、文本解析器和HTML解析器等。
优化器:优化器的目标是遍历AST,检测出所有静态子树(永远不会发生变化的DOM节点)并给其打标记。当AST中的静态子树被打上标记后,每次重新渲染时,就不需要为打上标记的静态节点创建新的虚拟节点,而是直接克隆已存在的虚拟节点。在虚拟DOM的更新操作中,如果发现两个节点是同一节点,正常情况下会对这两个节点进行更新,但是如果两个节点时静态节点,则可以直接跳过更新节点的流程。
代码生成器:代码生成器是模板编译的最后一步,它的作用是将AST转换成渲染函数中的内容,这个内容可以称为“代码字符串”。
例如:简单的模板
<p title='zjy' @click='btn'>zjy</p>
生成后的代码字符串
`with(this){return _c('p',{attrs:{"title":"zjy"},on:{"click":btn}},[_v("zjy")])}`
格式化后
with(this){ return _c('p',{ attrs:{"title":"zjy"}, on:{"click":btn} },[_v("zjy")]) }
这样一个代码字符串最终导出到界外使用时,会将代码字符串放到函数里,这个函数叫做渲染函数。
渲染函数的作用是创建vnode。渲染函数之所以可以生成vnode,是因为代码字符串中会有很多函数调用(例如,上面生成的代码字符串中有两个函数调用_c和_v),这些函数时虚拟DOM提供创建vnode的方法。vnode有很多种类型,不同类型对应不同的创建方法,所以代码字符串中的_c和_v其实都是创建vnode的方法,只是创建的vnode的类型不同。例如,_c可以创建元素类型的vnode,而_v可以创建文本类型的vnode。
简单的模板
<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)" }] }] }
HTML解析器的作用是解析HTML,它在解析HTML的过程中不断触发各种钩子函数。这些钩子函数包括开始标签钩子函数、结束标签钩子函数、文本钩子函数以及注释钩子函数。伪代码如下:
parseHTML(tempalte,{ start(tag,attrs,unary){ //unary:是否是自闭合标签 // 每当解析到标签的开始位置时,触发该函数 }, end(){ // 每当解析到标签的结束位置时,触发该函数 }, chars(text){ // 每当解析到文本时,触发该函数 }, comment(text){ // 每当解析到注释时,触发该函数 } })
HTML解析器的内部原理是一小段一小段地截取模板字符串,每截取一小段字符串,就会根据截取出来的字符串类型触发不同的钩子函数,知道模板字符串截空停止运行。
AST中type的取值及其说明
type的值 | 说明 |
1 | 元素节点 |
2 | 带变量的动态文本节点 |
3 | 不带变量的纯文本节点 |
解析模板的函数和生成代码字符串的函数就不能一一实现了。总之,解析模板得到AST,AST进行标记优化,然后对其执行函数得到得到代码字符串(`渲染函数`),执行渲染函数可以生成vnode,解析vnode可以得到DOM,从而进行页面渲染。