• 12.2 内联模板

    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函数。分析到这一步结论已经很清楚了。内联模板的内容最终会在子组件中解析,所以模板中可以拿到子组件的作用域这个现象也不足为奇了。