弄懂vue模板编译需要弄清
- 编译的目的
- 什么时候编译
- 编译过程
编译的目的
我们知道,vue的html部分的代码可以直接书写template字符串,也可以写render函数,那么template和render是什么关系?
new Vue({
el: '#app',
template: '<div>this is template</div>',
render(createElement) {
return createElement('div', 'this is render')
}
})
上面一段代码中,页面显示 'this is render',看来render优先级高于template,也就是说,写了render就会忽略template。
而render的目的是生成vnode对象,再遍历vnode生成页面元素,也就是render->vnode->dom。
那么只写template,没有render时,会是template->vnode->dom吗?个人理解,这里跟编译时机有关系
什么时候编译
编译方式有AOT和JIT,名字挺唬人的。
- AOT,ahead of time,就是提前编译,就是代码运行时,执行一段已经编译好的代码
- JIT ,just in time, 就是即时编译,就是运行的时候包括了编译过程
vue模板编译也不例外,可以提前编译和即时编译,所以vue有runtime版本和完整版本,runtime版本没有编译功能,体积更小,只用于运行已经编译的代码。
比如我们用webpack构建包时,vue-loader很重要的一个功能就是编译.vue文件中的template。回到编译目的,如果把template编译成一个vnode,这个vnode如此大,从服务器返回到页面,显然不合理,所以template应该是转成render函数(准确说是字符串,然后执行时用with函数解析),运行时再生成vnode,即template->render->vnode-dom。当然了,这只是基于源码后的一个分析,并不是推测出来的过程。
如果运行未编译好的vue项目,则需要使用包含编译的版本,一边运行一边编译
编译的过程
var createCompiler = createCompilerCreator(function baseCompile(
template,
options
) {
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});
步骤清晰明了
- 解析html,生成ast
- 对ast做一些优化
- 生成render函数字符串
解析html,生成ast
ast(abstract syntax code)即抽象语法树,一切概念都是纸老虎,它就是一个树状结构的对象,用于描述节点信息,比如
new Vue({
el: '#app',
template: "<div class='container'>this is template</div>",
})
template转成ast
有3类ast
- type为1的ASTElement,就是有tag的
- type为2的ASTExpression,就是表达式式,比如{{message}},指令如v-if、v-for
- type为3的ASTText,就是静态文本节点
解析html这个过程可谓繁琐,大概有几个要点
- 维护一个index,代表解析到哪个位置了
- 维护一个当前解析节点对象obj
- 维护一个stack=[],代表着解析深度
这里很关键的是stack
- 当遇到一个标签开始,如 <div ,则解析结果为 obj={tag:'div',end:false,children:[],...} ,end代表是否结束,当遇到 </div> 才结束。把obj push到stack里 。
- 接下来如果碰到又一个标签的开始,如<p,重复第1步。对于p,它在stack的前一项对应tag为div的那项,而它的end为false,所以把 p 作为 div 的children。
- 接下来如果遇到结束标签,则obj.end=true,stack pop最后一项,并且obj 始终指向stack最后一项
这个stack有点像,我们打开网页1,上面贴了网页2的地址,我们打开网页2,浏览完了关闭它,再回到网页1。后打开的先看完,并且能自动返回父窗口。
let template = "<div class='container'>" +
"<p class='item1'>{{message1}}</p>" +
"</div>"
以上面一段代码为例,维护一个栈stack,index=0,当前节点obj
- 用正则匹配到一个标签的开始,<div ,长度为4,index增加4,当前对象 obj={'tag':'div',end:false,children:[],...},并添加到stack里。
- 从index为4开始,在遇到 > 之前,中间的内容都收集做为当前对象的属性,如 class='container' ,同时index往前走,直到遇到 > ,说明该对象属性收集完毕
- 继续用正则匹配,发现了<p ,又是一个标签的开始,obj={'tag':'p',end:false,children:[],...},并添加到stack里。然后收集属性,直到遇到 > 。因为stack里上一项div对应的obj,end为false,所以需要将当前对象 p对应的obj添加到div的children里。
- 然后遇到 {{ ,它不是一个标签开始,所以在匹配到标签之前,把所以内容都作为一个节点。同理,因为 p 对应obj的end为false,把这部分收集到 p 的children里。
- 继续匹配,匹配到 </p> ,是一个结束标签,p 匹配结束,让它end为true, stack长度减 1,obj改变指向为stack最后一项 ... 匹配到 </div> ,同理。
- template被解析完,收工
对ast做一些优化
我们知道,vue 重新渲染,会有一个 diff 过程,就是比较新旧vnode对象,然后只针对差异部分进行dom处理。
而对ast做优化就是给ast对象添加一个标记,如果我们可以预知这个节点永远不会更新,那么我们既可以标记它的static为true,然后diff过程中直接将它跳过。
优化分为两步
- 遍历所有节点,标记是否为静态节点
- 遍历节点,判断是否为静态根节点
//上面说过有3种节点
function isStatic(node) {
if (node.type === 2) { // 表单式节点 expression
return false
}
if (node.type === 3) { // 文本节点 text
return true
}
// 剩下就是标签节点
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
function markStatic(node) {
node.static = isStatic(node);
if (node.type === 1) {
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
// 遍历子节点,若有一个子节点不是static,那么父节点不能为static
for (var i = 0, l = node.children.length; i < l; i++) {
var child = node.children[i];
markStatic$1(child);
if (!child.static) {
node.static = false;
}
}
if (node.ifConditions) {
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
var block = node.ifConditions[i$1].block;
markStatic$1(block);
if (!block.static) {
node.static = false;
}
}
}
}
}
除了 node.ifConditions ,其他都好理解。按道理有v-if,static应该为false。个人理解,这里是为了区分 表达式v-if="true" 和 字符串v-if="'true'" 。
function markStaticRoots(node, isInFor) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor;
}
// For a node to qualify(合格; 使合格; 使具备资格) as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting(提升) out will
// outweigh the benefits and it's better off to just always render it fresh.
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true;
return
} else {
node.staticRoot = false;
}
if (node.children) {
for (var i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for);
}
}
if (node.ifConditions) {
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
markStaticRoots(node.ifConditions[i$1].block, isInFor);
}
}
}
}
注意这里的静态根节点,是一个相对子节点来说的根节点,并非顶层节点那种意思。
注释翻译过来,就是,一个节点static为true,并且有超过1个children,并且第一个child的type不是text时,才标记staticRoot为true,否则这种静态根节点预处理带来的消耗比收益低。这里应该是减少遍历?因为绝大部分节点都会有children。先留个坑。
生成render函数字符串
将ast标记优化好后,遍历ast树,将节点变成一个等待调用的函数,该函数用于创建该节点
几种内部方法
_c:对应的是 createElement 方法,顾名思义,它的含义是创建一个元素(Vnode)
_v:创建一个文本结点。
_s:把一个值转换为字符串。(eg: {{data}})
_m:渲染静态内容
把这几个方法写的这么短的目的是减少代码体积,因为生成的是render函数字符串,字符串在打包时时不会被压缩的。
<template>
<div class="container">
{{message1}}
</div>
</template>
会被转成字符串
let code ='_c('div',{staticClass:"container"},[_v(_s(message1))])'
render = ("with(this){return " + code + "}")
当调用vue的$mount时,通过new Fucntion就把render字符串当作函数来执行,调用里面的_c、_v、_s 方法,创建vnode,然后创建真实dom。