- 12.2 内联模板
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>
// jsvar 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为nullvar 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函数和staticRenderFnsif (isDef(inlineTemplate)) {options.render = inlineTemplate.render;options.staticRenderFns = inlineTemplate.staticRenderFns;}// 执行vue子组件实例化return new vnode.componentOptions.Ctor(options)}
子组件的默认选项配置会根据vnode上的inlineTemplate属性拿到模板的render函数。分析到这一步结论已经很清楚了。内联模板的内容最终会在子组件中解析,所以模板中可以拿到子组件的作用域这个现象也不足为奇了。
