Vue 2.0 深入源码分析(三) 基础篇 模板渲染 el、emplate、render属性详解
用法
vue有三个属性和模板有关,官网上是这样解释的:
el ;提供一个在页面上已存在的 dom 元素作为 vue 实例的挂载目标
template ;一个字符串模板作为 vue 实例的标识使用。模板将会 替换 挂载的元素。挂载元素的内容都将被忽略,除非模板的内容有分发插槽。
render ;字符串模板的代替方案,允许你发挥 javascript 最大的编程能力。该渲染函数接收一个 createelement
方法作为第一个参数用来创建 vnode
。
简单说一下,就是:
vue内部会判断如果没有render属性则把template属性的值作为模板,如果template不存在则把el对应的dom节点的outerhtml属性作为模板,经过一系列正则解析和流程生成一个render函数,最后通过with(this){}来执行。
也就是说template的优先级大于el。
render的参数是vue内部的$createelement函数(位于4486行),它的可扩展性更强一些,在一些项目的需求中,可以用很简单的代码得到一个模板。例如vue实战9.3里介绍的例子,有兴趣可以看看
render可以带4个参数,分别如下:
tag ;元素的标签名,也可以是组件名
data ;该vnode的属性,是个对象
children ;子节点,是个数组
、其中参数2可以省略的,在4335行做了修正,最后执行_createelement()函数,如下:
function createelement ( //第4335行 context, tag, data, children, normalizationtype, alwaysnormalize ) { if (array.isarray(data) || isprimitive(data)) { //如果data是个数组或者是基本类型 normalizationtype = children; children = data; //修正data为children data = undefined; //修正data为undefined } if (istrue(alwaysnormalize)) { normalizationtype = always_normalize; } return _createelement(context, tag, data, children, normalizationtype) //最后执行_createelement创建一个虚拟vnode }
例如下面三个vue实例,分别用el、template和rentder指定模板,它们的输出是一样的
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <script src="https://cdn.bootcss.com/vue/2.5.16/vue.js"></script> <title>document</title> </head> <body> <div id="app1">{{message}}</div> <div id="app2"></div> <div id="app3"></div> <script> var data={message:'you are so annoying'} new vue({el:'#app1',data}) //用el做模板 new vue({el:'#app2',data,template:"<div>{{message}}</div>"}) //用template做模板 new vue({el:'#app3',data,render:function(h){return h('div',this.message)}}) //直接用render函数指定模板 </script> </body> </html>
、浏览器显示结果:
可以看到输出是一摸一样的
vue实例后会先执行_init()进行初始化,快结束时会判断是否有el属性,如果存在则调用$mount进行挂载,$mount函数如下:
vue.prototype.$mount = function ( //定义在10861行 el, hydrating ) { el = el && query(el); /* istanbul ignore if */ if (el === document.body || el === document.documentelement) { "development" !== 'production' && warn( "do not mount vue to <html> or <body> - mount to normal elements instead." ); return this } var options = this.$options; // resolve template/el and convert to render function if (!options.render) { //如果render属性不存在 var template = options.template; //则尝试获取template属性并将其编译成render if (template) { if (typeof template === 'string') { if (template.charat(0) === '#') { template = idtotemplate(template); /* istanbul ignore if */ if ("development" !== 'production' && !template) { warn( ("template element not found or is empty: " + (options.template)), this ); } } } else if (template.nodetype) { template = template.innerhtml; } else { { warn('invalid template option:' + template, this); } return this } } else if (el) { //如果templtate不存在但是el存在,则获取调用getouterhtml()函数获取el的outerhtml属性,getouterhtml()定义在10933行,也就是末尾,用户获取dom的outerhtml template = getouterhtml(el); } if (template) { /* istanbul ignore if */ if ("development" !== 'production' && config.performance && mark) { mark('compile'); } var ref = compiletofunctions(template, { shoulddecodenewlines: shoulddecodenewlines, shoulddecodenewlinesforhref: shoulddecodenewlinesforhref, delimiters: options.delimiters, comments: options.comments }, this); //这里调用compiletofunctions()将template解析成一个render函数,并返回 var render = ref.render; var staticrenderfns = ref.staticrenderfns; options.render = render; options.staticrenderfns = staticrenderfns; /* istanbul ignore if */ if ("development" !== 'production' && config.performance && mark) { mark('compile end'); measure(("vue " + (this._name) + " compile"), 'compile', 'compile end'); } } } return mount.call(this, el, hydrating) };
compiletofunctions函数是由createcompiler()返回的(这里有点绕,研究代码的时候在里面绕了好几天),我把大致主体贴出来,如下:
var baseoptions ={} //编译的配置项 第9802行 function createcompiletofunctionfn(compile){ var cache = object.create(null); return return function compiletofunctions(template, options, vm) { //编译时先执行这里 /**/ compile(template,options) /**/ } } function createcompilercreator(basecompile){ return function(baseoptions){ function compile(template, options) {/**/} return { compile: compile, compiletofunctions: createcompiletofunctionfn(compile) //难点2:匿名函数返回的值中又调用了createcompiletofunctionfn函数 } } } var createcompiler = createcompilercreator(function(){ //传入一个匿名函数 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} //最后返回一个对象 }) var ref$1 = createcompiler(baseoptions); var compiletofunctions = ref$1.compiletofunctions; //编译的入口文件
是不是有点晕呢,我举一个例子就能看明白了,如下:
function show(show){ //shou函数也直接返回一个匿名函数,带一个参数 return function(info){ show(info) //show通过作用域链就可以访问到参数的show函数了 } } var info=show(function(info){ console.log(info) }) //这里执行show函数,传入一个匿名函数 info({name:'gsz'}) //控制台输出:{name: "gsz"}
vue内部看得晦涩是因为传参的时候都注明了一个函数名,其实这个函数名是可以忽略的,这样看起来会更清晰一点 注:这样设计是为了跨平台一些代码的复用和存放吧,代码结构在node下更好理解一点
compiletofunctions函数内部会调用parse()将模板经过一系列的正则解析,用一个ast对象保存,然后调用generate()做静态节点标记,最后调用generate生成一个render函数
以上面的第一个vue实例来说,parse()解析后的ast对象如下:
、再通过generate()后生成如下一个对象,其中render就是最终要执行的render函数了
compiletofunctions函数返回值是一个对象,以上面的第一个vue实例为例,返回后的信息如下:
{
render:"(function anonymous() {with(this){return _c('div',{attrs:{"id":"app1"}},[_v(_s(message))])}})", //最终渲染出来的render函数
staticrenderfns:function[] //如果是静态节点,则保存到这里
}
以后分析到每个api时这里会单独分析的
最后在mountcomponent()函数内会以当前vue实例为上下文,执行该render函数(在2739行),此时就会完成渲染watch的收集,并生成虚拟vnode,最后调用_update()方法生成真实dom节点。
推荐阅读
-
Vue 2.0 深入源码分析(七) 基础篇 侦听器 watch属性详解
-
Vue 2.0 深入源码分析(五) 基础篇 methods属性详解
-
Vue 2.0 深入源码分析(六) 基础篇 computed 属性详解
-
Vue 2.0 深入源码分析(三) 基础篇 模板渲染 el、emplate、render属性详解
-
Vue 2.0 深入源码分析(七) 基础篇 侦听器 watch属性详解
-
Vue 2.0 深入源码分析(六) 基础篇 computed 属性详解
-
Vue 2.0 深入源码分析(五) 基础篇 methods属性详解
-
Vue 2.0 深入源码分析(三) 基础篇 模板渲染 el、emplate、render属性详解