站长资源网络编程

深入浅析Vue中的slots/scoped slots

整理:jimmy2026/7/31浏览2
简介一直对Vue中的slot插槽比较感兴趣,下面是自己的一些简单理解,希望可以帮助大家更好的理解slot插槽下面结合一个例子,简单说明slots的工作原理dx-li子组件的template如下:
  • 你好!
  • dx-ul父组件的tem

    一直对Vue中的slot插槽比较感兴趣,下面是自己的一些简单理解,希望可以帮助大家更好的理解slot插槽

    下面结合一个例子,简单说明slots的工作原理

    dx-li子组件的template如下:

    <li class="dx-li">
     <slot>
       你好!
     </slot>
    </li>
    dx-ul父组件的template如下:
    <ul>
     <dx-li>
      hello juejin!
     </dx-li>
    </ul>
    结合上述例子以及vue中相关源码进行分析
    dx-ul父组件中template编译后,生成的组件render函数:
    module.exports={
     render:function (){
      var _vm=this;
      var _h=_vm.$createElement;
      var _c=_vm._self._c||_h;
      // 其中_vm.v为createTextVNode创建文本VNode的函数
      return _c('ul', 
        [_c('dx-li', [_vm._v("hello juejin!")])],
        1)
     },
     staticRenderFns: []
    }

    传递的插槽内容'hello juejin!'会被编译成dx-li子组件VNode节点的子节点。

    渲染dx-li子组件,其中子组件的render函数:

    module.exports={
     render:function (){
      var _vm=this;
      var _h=_vm.$createElement;
      var _c=_vm._self._c||_h;
      // 其中_vm._v 函数为renderSlot函数
      return _c('li', 
        {staticClass: "dx-li" }, 
        [_vm._t("default", [_vm._v("你好 掘金!")])], 
        2
       )
      },
     staticRenderFns: []
    }

    初始化dx-li子组件vue实例过程中,会调用initRender函数:

    function initRender (vm) {
     ...
     // 其中_renderChildren数组,存储为 'hello juejin!'的VNode节点;renderContext一般为父组件Vue实例
     这里为dx-ul组件实例
     vm.$slots = resolveSlots(options._renderChildren, renderContext);
     ...
    }

    其中resolveSlots函数为:

    /**
     * 主要作用是将children VNodes转化成一个slots对象.
     */
    export function resolveSlots (
     children: "abc"></span> 
     * 编译成span的VNode节点data = {attrs:{slot: "abc"}, slot: "abc"},所以这里删除该节点attrs的slot
     */
     if (data && data.attrs && data.attrs.slot) {
      delete data.attrs.slot
     }
     /* 判断是否为具名插槽,如果为具名插槽,还需要子组件/函数子组件渲染上下文一致。主要作用:
     *当需要向子组件的子组件传递具名插槽时,不会保持插槽的名字。
     * 举个栗子:
     * child组件template: 
     * <div>
     * <div class="default"><slot></slot></div>
     * <div class="named"><slot name="foo"></slot></div>
     * </div>
     * parent组件template:
     * <child><slot name="foo"></slot></child>
     * main组件template:
     * <parent><span slot="foo">foo</span></parent>
     * 此时main渲染的结果:
     * <div>
     * <div class="default"><span slot="foo">foo</span></div>
       <div class="named"></div>
     * </div>
     */
     if ((child.context === context || child.fnContext === context) &&
      data && data.slot != null
     ) {
      const name = data.slot
      const slot = (slots[name] || (slots[name] = []))
      // 这里处理父组件采用template形式的插槽
      if (child.tag === 'template') {
      slot.push.apply(slot, child.children || [])
      } else {
      slot.push(child)
      }
     } else {
      // 返回匿名default插槽VNode数组
      (slots.default || (slots.default = [])).push(child)
     }
     }
     // 忽略仅仅包含whitespace的插槽
     for (const name in slots) {
     if (slots[name].every(isWhitespace)) {
      delete slots[name]
     }
     }
     return slots
    }

    然后挂载dx-li组件时,会调用dx-li组件render函数,在此过程中会调用renderSlot函数:

    export function renderSlot (
      name: string, // 子组件中slot的name,匿名default
      fallback: "obj"></slot> obj必须是一个对象
     ): "${name}" found in the same render tree ` +
        `- this will likely cause render errors.`,
        this
       )
       }
       // 设置父组件传递插槽的VNode._rendered,用于后面判断是否有重名slot
       slotNodes._rendered = true
      }
      // 如果没有传入插槽,则为默认插槽内容VNode
      nodes = slotNodes || fallback
      }
      // 如果还需要向子组件的子组件传递slot
      /*举个栗子:
      * Bar组件: <div class="bar"><slot name="foo"/></div>
      * Foo组件:<div class="foo"><bar><slot slot="foo"/></bar></div>
      * main组件:<div><foo>hello</foo></div>
      * 最终渲染:<div class="foo"><div class="bar">hello</div></div>
      */
      const target = props && props.slot
      if (target) {
      return this.$createElement('template', { slot: target }, nodes)
      } else {
      return nodes
      }
     }

    scoped slots理解

    dx-li子组件的template如下:

    <li class="dx-li"> 
     <slot str="你好 掘金!">
      hello juejin!
     </slot>
    </li>
    dx-ul父组件的template如下:
    <ul>
     <dx-li>
      <span slot-scope="scope">
       {{scope.str}}
      </span>
     </dx-li>
    </ul>
    结合例子和Vue源码简单作用域插槽
    dx-ul父组件中template编译后,产生组件render函数:
    module.exports={
     render:function (){
      var _vm=this;
      var _h=_vm.$createElement;
      var _c=_vm._self._c||_h;
       return _c('ul', [_c('dx-li', {
       // 可以编译生成一个对象数组
       scopedSlots: _vm._u([{
        key: "default",
        fn: function(scope) {
        return _c('span', 
         {},
         [_vm._v(_vm._s(scope.str))]
        )
        }
       }])
       })], 1)
      },
     staticRenderFns: []
     }

    其中 _vm._u函数:

    function resolveScopedSlots (
     fns, // 为一个对象数组,见上文scopedSlots
     res
    ) {
     res = res || {};
     for (var i = 0; i < fns.length; i++) {
      if (Array.isArray(fns[i])) {
       // 递归调用
       resolveScopedSlots(fns[i], res);
      } else {
       res[fns[i].key] = fns[i].fn;
      }
     }
     return res
    }

    子组件的后续渲染过程与slots类似。scoped slots原理与slots基本是一致,不同的是编译父组件模板时,会生成一个返回结果为VNode的函数。当子组件匹配到父组件传递作用域插槽函数时,调用该函数生成对应VNode。

    总结

    其实slots/scoped slots 原理是非常简单的,我们只需明白一点vue在渲染组件时,是根据VNode渲染实际DOM元素的。

    slots是将父组件编译生成的插槽VNode,在渲染子组件时,放置到对应子组件渲染VNode树中。

    scoped slots是将父组件中插槽内容编译成一个函数,在渲染子组件时,传入子组件props,生成对应的VNode。最后子组件,根据组件render函数返回VNode节点树,update渲染真实DOM元素。同时,可以看出跨组件传递插槽也是可以的,但是必须注意具名插槽传递。