• 10.3 具名插槽
    • 10.3.1 模板编译的差别
    • 10.3.2 父组件vnode生成阶段
    • 10.3.3 子组件渲染Vnode过程
    • 10.3.4 子组件渲染真实dom

    10.3 具名插槽

    往往我们需要灵活的使用插槽进行通用组件的开发,要求父组件每个模板对应子组件中每个插槽,这时我们可以使用<slot>name属性,同样举个简单的例子。

    1. var child = {
    2. template: `<div class="child"><slot name="header"></slot><slot name="footer"></slot></div>`,
    3. }
    4. var vm = new Vue({
    5. el: '#app',
    6. components: {
    7. child
    8. },
    9. template: `<div id="app"><child><template v-slot:header><span>头部</span></template><template v-slot:footer><span>底部</span></template></child></div>`,
    10. })

    渲染结果:

    1. <div class="child"><span>头部</span><span>底部</span></div>

    接下来我们在普通插槽的基础上,看看源码在具名插槽实现上的区别。

    10.3.1 模板编译的差别

    父组件在编译AST阶段和普通节点的过程不同,具名插槽一般会在template模板中用v-slot:来标注指定插槽,这一阶段会在编译阶段特殊处理。最终的AST树会携带scopedSlots用来记录具名插槽的内容

    1. {
    2. scopedSlots {
    3. footer: { ··· },
    4. header: { ··· }
    5. }
    6. }

    AST生成render函数的过程也不详细分析了,我们只分析父组件最终返回的结果(如果对parse, generate感兴趣的同学,可以直接看源码分析,编译阶段冗长且难以讲解,跳过这部分分析)

    1. with(this){return _c('div',{attrs:{"id":"app"}},[_c('child',{scopedSlots:_u([{key:"header",fn:function(){return [_c('span',[_v("头部")])]},proxy:true},{key:"footer",fn:function(){return [_c('span',[_v("底部")])]},proxy:true}])})],1)}

    很明显,父组件的插槽内容用_u函数封装成数组的形式,并赋值到scopedSlots属性中,而每一个插槽以对象形式描述,key代表插槽名,fn是一个返回执行结果的函数。

    10.3.2 父组件vnode生成阶段

    照例进入父组件生成Vnode阶段,其中_u函数的原形是resolveScopedSlots,其中第一个参数就是插槽数组。

    1. // vnode生成阶段针对具名插槽的处理 _u (target._u = resolveScopedSlots)
    2. function resolveScopedSlots (fns,res,hasDynamicKeys,contentHashKey) {
    3. res = res || { $stable: !hasDynamicKeys };
    4. for (var i = 0; i < fns.length; i++) {
    5. var slot = fns[i];
    6. // fn是数组需要递归处理。
    7. if (Array.isArray(slot)) {
    8. resolveScopedSlots(slot, res, hasDynamicKeys);
    9. } else if (slot) {
    10. // marker for reverse proxying v-slot without scope on this.$slots
    11. if (slot.proxy) { // 针对proxy的处理
    12. slot.fn.proxy = true;
    13. }
    14. // 最终返回一个对象,对象以slotname作为属性,以fn作为值
    15. res[slot.key] = slot.fn;
    16. }
    17. }
    18. if (contentHashKey) {
    19. (res).$key = contentHashKey;
    20. }
    21. return res
    22. }

    最终父组件的vnode节点的data属性上多了scopedSlots数组。回顾一下,具名插槽和普通插槽实现上有明显的不同,普通插槽是以componentOptions.child的形式保留在父组件中,而具名插槽是以scopedSlots属性的形式存储到data属性中。

    1. // vnode
    2. {
    3. scopedSlots: [{
    4. 'header': fn,
    5. 'footer': fn
    6. }]
    7. }

    10.3.3 子组件渲染Vnode过程

    子组件在解析成AST树阶段的不同,在于对slot标签的name属性的解析,而在render生成Vnode过程中,slot的规范化处理针对具名插槽会进行特殊的处理,回到normalizeScopedSlots的代码

    1. vm.$scopedSlots = normalizeScopedSlots(
    2. _parentVnode.data.scopedSlots, // 此时的第一个参数会拿到父组件插槽相关的数据
    3. vm.$slots, // 记录父组件的插槽内容
    4. vm.$scopedSlots
    5. );

    最终子组件实例上的$scopedSlots属性会携带父组件插槽相关的内容。

    1. // 子组件Vnode
    2. {
    3. $scopedSlots: [{
    4. 'header': f,
    5. 'footer': f
    6. }]
    7. }

    10.3.4 子组件渲染真实dom

    和普通插槽类似,子组件渲染真实节点的过程会执行子render函数中的_t方法,这部分的源码会和普通插槽走不同的分支,其中this.$scopedSlots根据上面分析会记录着父组件插槽内容相关的数据,所以会和普通插槽走不同的分支。而最终的核心是执行nodes = scopedSlotFn(props),也就是执行function(){return [_c('span',[_v("头部")])]},具名插槽之所以是函数的形式执行而不是直接返回结果,我们在后面揭晓。

    1. function renderSlot (
    2. name,
    3. fallback, // slot插槽后备内容
    4. props, // 子传给父的值
    5. bindObject
    6. ){
    7. var scopedSlotFn = this.$scopedSlots[name];
    8. var nodes;
    9. // 针对具名插槽,特点是$scopedSlots有值
    10. if (scopedSlotFn) { // scoped slot
    11. props = props || {};
    12. if (bindObject) {
    13. if (!isObject(bindObject)) {
    14. warn('slot v-bind without argument expects an Object',this);
    15. }
    16. props = extend(extend({}, bindObject), props);
    17. }
    18. // 执行时将子组件传递给父组件的值传入fn
    19. nodes = scopedSlotFn(props) || fallback;
    20. }···
    21. }

    至此子组件通过slotName找到了对应父组件的插槽内容。