弄懂vue模板编译需要弄清
我们知道,vue的html部分的代码可以直接书写template字符串,也可以写render函数,那么template和render是什么关系?
new Vue({
el: '#app',
template: 'this is template',
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,名字挺唬人的。
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
}
});
步骤清晰明了
ast(abstract syntax code)即抽象语法树,一切概念都是纸老虎,它就是一个树状结构的对象,用于描述节点信息,比如
new Vue({
el: '#app',
template: "this is template",
})
template转成ast
有3类ast
解析html这个过程可谓繁琐,大概有几个要点
这里很关键的是stack
,重复第1步。对于p,它在stack的前一项对应tag为div的那项,而它的end为false,所以把 p 作为 div 的children。
这个stack有点像,我们打开网页1,上面贴了网页2的地址,我们打开网页2,浏览完了关闭它,再回到网页1。后打开的先看完,并且能自动返回父窗口。
let template = "" +
"{{message1}}
" +
""
以上面一段代码为例,维护一个栈stack,index=0,当前节点obj
,又是一个标签的开始,obj={'tag':'p',end:false,children:[],...},并添加到stack里。然后收集属性,直到遇到 > 。因为stack里上一项div对应的obj,end为false,所以需要将当前对象 p对应的obj添加到div的children里。
我们知道,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。先留个坑。
将ast标记优化好后,遍历ast树,将节点变成一个等待调用的函数,该函数用于创建该节点
几种内部方法
_c:对应的是 createElement 方法,顾名思义,它的含义是创建一个元素(Vnode)
_v:创建一个文本结点。
_s:把一个值转换为字符串。(eg: {{data}})
_m:渲染静态内容
把这几个方法写的这么短的目的是减少代码体积,因为生成的是render函数字符串,字符串在打包时时不会被压缩的。
{{message1}}
会被转成字符串
let code ='_c('div',{staticClass:"container"},[_v(_s(message1))])'
render = ("with(this){return " + code + "}")
当调用vue的$mount时,通过new Fucntion就把render字符串当作函数来执行,调用里面的_c、_v、_s 方法,创建vnode,然后创建真实dom。