12.2 内联模板

由于动态组件除了有is作为传值外,还可以有inline-template作为配置,借此前提,刚好可以理清楚Vue中内联模板的原理和设计思想。Vue在官网有一句醒目的话,提示我们inline-template 会让模板的作用域变得更加难以理解。因此建议尽量使用template选项来定义模板,而不是用内联模板的形式。接下来,我们通过源码去定位一下所谓作用域难以理解的原因。

我们先简单调整上面的例子,从使用角度上入手:

  1. // html
  2. <div id="app">
  3. <button @click="changeTabs('child1')">child1</button>
  4. <button @click="changeTabs('child2')">child2</button>
  5. <button @click="changeTabs('child3')">child3</button>
  6. <component :is="chooseTabs" inline-template>
  7. <span>{{test}}</span>
  8. </component>
  9. </div>
  1. // js
  2. var child1 = {
  3. data() {
  4. return {
  5. test: 'content1'
  6. }
  7. }
  8. }
  9. var child2 = {
  10. data() {
  11. return {
  12. test: 'content2'
  13. }
  14. }
  15. }
  16. var child3 = {
  17. data() {
  18. return {
  19. test: 'content3'
  20. }
  21. }
  22. }
  23. var vm = new Vue({
  24. el: '#app',
  25. components: {
  26. child1,
  27. child2,
  28. child3
  29. },
  30. data() {
  31. return {
  32. chooseTabs: 'child1',
  33. }
  34. },
  35. methods: {
  36. changeTabs(tab) {
  37. this.chooseTabs = tab;
  38. }
  39. }
  40. })

例子中达到的效果和文章第一个例子一致,很明显和以往认知最大的差异在于,父组件里的环境可以访问到子组件内部的环境变量。初看觉得挺不可思议的。我们回忆一下之前父组件能访问到子组件的情形,从大的方向上有两个:

1. 采用事件机制,子组件通过$emit事件,将子组件的状态告知父组件,达到父访问子的目的。

2. 利用作用域插槽的方式,将子的变量通过props的形式传递给父,而父通过v-slot的语法糖去接收,而我们之前分析的结果是,这种方式本质上还是通过事件派发的形式去通知父组件。

之前分析过程也有提过父组件无法访问到子环境的变量,其核心的原因在于:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。那么我们有理由猜想,内联模板是不是违背了这一原则,让父的内容放到了子组件创建过程去编译呢?我们接着往下看:

回到ast解析阶段,前面分析到,针对动态组件的解析,关键在于processComponent函数对is属性的处理,其中还有一个关键是对inline-template的处理,它会在ast树上增加inlineTemplate属性。

  1. // 针对动态组件的解析
  2. function processComponent (el) {
  3. var binding;
  4. // 拿到is属性所对应的值
  5. if ((binding = getBindingAttr(el, 'is'))) {
  6. // ast树上多了component的属性
  7. el.component = binding;
  8. }
  9. // 添加inlineTemplate属性
  10. if (getAndRemoveAttr(el, 'inline-template') != null) {
  11. el.inlineTemplate = true;
  12. }
  13. }

render函数生成阶段由于inlineTemplate的存在,父的render函数的子节点为null,这一步也决定了inline-template下的模板并不是在父组件阶段编译的,那模板是如何传递到子组件的编译过程呢?答案是模板以属性的形式存在,待到子实例时拿到属性值

  1. function genComponent (componentName,el,state) {
  2. // 拥有inlineTemplate属性时,children为null
  3. var children = el.inlineTemplate ? null : genChildren(el, state, true);
  4. return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")")
  5. }

我们看看最终render函数的结果,其中模板以{render: function(){···}}的形式存在于父组件的inlineTemplate属性中。

  1. "_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属性中。

  1. // vnode结果
  2. {
  3. data: {
  4. inlineTemplate: {
  5. render: function() {}
  6. },
  7. tag: 'component'
  8. },
  9. tag: "vue-component-1-child1"
  10. }

有了vnode后,来到了关键的最后一步,根据vnode生成真实节点的过程。从根节点开始,遇到vue-component-1-child1,会经历实例化创建子组件的过程,实例化子组件前会先对inlineTemplate属性进行处理。

  1. function createComponentInstanceForVnode (vnode,parent) {
  2. // 子组件的默认选项
  3. var options = {
  4. _isComponent: true,
  5. _parentVnode: vnode,
  6. parent: parent
  7. };
  8. var inlineTemplate = vnode.data.inlineTemplate;
  9. // 内联模板的处理,分别拿到render函数和staticRenderFns
  10. if (isDef(inlineTemplate)) {
  11. options.render = inlineTemplate.render;
  12. options.staticRenderFns = inlineTemplate.staticRenderFns;
  13. }
  14. // 执行vue子组件实例化
  15. return new vnode.componentOptions.Ctor(options)
  16. }

子组件的默认选项配置会根据vnode上的inlineTemplate属性拿到模板的render函数。分析到这一步结论已经很清楚了。内联模板的内容最终会在子组件中解析,所以模板中可以拿到子组件的作用域这个现象也不足为奇了。