12.2 内联模板
由于动态组件除了有is
作为传值外,还可以有inline-template
作为配置,借此前提,刚好可以理清楚Vue
中内联模板的原理和设计思想。Vue
在官网有一句醒目的话,提示我们inline-template
会让模板的作用域变得更加难以理解。因此建议尽量使用template
选项来定义模板,而不是用内联模板的形式。接下来,我们通过源码去定位一下所谓作用域难以理解的原因。
我们先简单调整上面的例子,从使用角度上入手:
// html
<div id="app">
<button @click="changeTabs('child1')">child1</button>
<button @click="changeTabs('child2')">child2</button>
<button @click="changeTabs('child3')">child3</button>
<component :is="chooseTabs" inline-template>
<span>{{test}}</span>
</component>
</div>
// js
var child1 = {
data() {
return {
test: 'content1'
}
}
}
var child2 = {
data() {
return {
test: 'content2'
}
}
}
var child3 = {
data() {
return {
test: 'content3'
}
}
}
var vm = new Vue({
el: '#app',
components: {
child1,
child2,
child3
},
data() {
return {
chooseTabs: 'child1',
}
},
methods: {
changeTabs(tab) {
this.chooseTabs = tab;
}
}
})
例子中达到的效果和文章第一个例子一致,很明显和以往认知最大的差异在于,父组件里的环境可以访问到子组件内部的环境变量。初看觉得挺不可思议的。我们回忆一下之前父组件能访问到子组件的情形,从大的方向上有两个:
1. 采用事件机制,子组件通过$emit
事件,将子组件的状态告知父组件,达到父访问子的目的。
2. 利用作用域插槽的方式,将子的变量通过props
的形式传递给父,而父通过v-slot
的语法糖去接收,而我们之前分析的结果是,这种方式本质上还是通过事件派发的形式去通知父组件。
之前分析过程也有提过父组件无法访问到子环境的变量,其核心的原因在于:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。那么我们有理由猜想,内联模板是不是违背了这一原则,让父的内容放到了子组件创建过程去编译呢?我们接着往下看:
回到ast
解析阶段,前面分析到,针对动态组件的解析,关键在于processComponent
函数对is
属性的处理,其中还有一个关键是对inline-template
的处理,它会在ast
树上增加inlineTemplate
属性。
// 针对动态组件的解析
function processComponent (el) {
var binding;
// 拿到is属性所对应的值
if ((binding = getBindingAttr(el, 'is'))) {
// ast树上多了component的属性
el.component = binding;
}
// 添加inlineTemplate属性
if (getAndRemoveAttr(el, 'inline-template') != null) {
el.inlineTemplate = true;
}
}
render
函数生成阶段由于inlineTemplate
的存在,父的render
函数的子节点为null
,这一步也决定了inline-template
下的模板并不是在父组件阶段编译的,那模板是如何传递到子组件的编译过程呢?答案是模板以属性的形式存在,待到子实例时拿到属性值
function genComponent (componentName,el,state) {
// 拥有inlineTemplate属性时,children为null
var children = el.inlineTemplate ? null : genChildren(el, state, true);
return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")")
}
我们看看最终render
函数的结果,其中模板以{render: function(){···}}
的形式存在于父组件的inlineTemplate
属性中。
"_c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component",inlineTemplate:{render:function(){with(this){return _c('span',[_v(_s(test))])}},staticRenderFns:[]}})],1)"
最终vnode
结果也显示,inlineTemplate
对象会保留在父组件的data
属性中。
// vnode结果
{
data: {
inlineTemplate: {
render: function() {}
},
tag: 'component'
},
tag: "vue-component-1-child1"
}
有了vnode
后,来到了关键的最后一步,根据vnode
生成真实节点的过程。从根节点开始,遇到vue-component-1-child1
,会经历实例化创建子组件的过程,实例化子组件前会先对inlineTemplate
属性进行处理。
function createComponentInstanceForVnode (vnode,parent) {
// 子组件的默认选项
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
var inlineTemplate = vnode.data.inlineTemplate;
// 内联模板的处理,分别拿到render函数和staticRenderFns
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
// 执行vue子组件实例化
return new vnode.componentOptions.Ctor(options)
}
子组件的默认选项配置会根据vnode
上的inlineTemplate
属性拿到模板的render
函数。分析到这一步结论已经很清楚了。内联模板的内容最终会在子组件中解析,所以模板中可以拿到子组件的作用域这个现象也不足为奇了。