深入理解Vue中的slots/scoped slots

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

下面结合一个例子,简单说明slots的工作原理
  1. dx-li子组件的template如下:
<li class="dx-li">
    <slot>
         你好 掘金!
    </slot>
</li>
复制代码
  1. dx-ul父组件的template如下:
<ul>
    <dx-li>
        hello juejin!
    </dx-li>
</ul>
复制代码
  1. 结合上述例子以及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: ?Array<VNode>,
      context: ?Component
    ): { [key: string]: Array<VNode> } {
      const slots = {}
      // 判断是否有children,即是否有插槽VNode
      if (!children) {
        return slots
      }
      // 遍历父组件节点的孩子节点
      for (let i = 0, l = children.length; i < l; i++) {
        const child = children[i]
        // data为VNodeData,保存父组件传递到子组件的props以及attrs等
        const data = child.data
        /* 移除slot属性
        * <span slot="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: ?Array<VNode>, // 子组件插槽中默认内容VNode数组,如果没有插槽内容,则显示该内容
          props: ?Object, // 子组件传递到插槽的props
          bindObject: ?Object // 针对<slot v-bind="obj"></slot> obj必须是一个对象
        ): ?Array<VNode> {
        // 判断父组件是否传递作用域插槽
          const scopedSlotFn = this.$scopedSlots[name]
          let nodes
          if (scopedSlotFn) { // scoped slot
            props = props || {}
            if (bindObject) {
              if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
                warn(
                  'slot v-bind without argument expects an Object',
                  this
                )
              }
              props = extend(extend({}, bindObject), props)
            }
            // 传入props生成相应的VNode
            nodes = scopedSlotFn(props) || fallback
          } else {
            // 如果父组件没有传递作用域插槽
            const slotNodes = this.$slots[name]
            // warn duplicate slot usage
            if (slotNodes) {
              if (process.env.NODE_ENV !== 'production' && slotNodes._rendered) {
                warn(
                  `Duplicate presence of slot "${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理解
  1. dx-li子组件的template如下:
<li class="dx-li">	
    <slot str="你好 掘金!">
	    hello juejin!
    </slot>
</li>   
复制代码
  1. dx-ul父组件的template如下:
<ul>
    <dx-li>
        <span slot-scope="scope">
            {{scope.str}}
        </span>
    </dx-li>
</ul>
复制代码
  1. 结合例子和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元素。同时,可以看出跨组件传递插槽也是可以的,但是必须注意具名插槽传递

以上是本人对于Slots的一些浅显理解,关于slot还有很多其他的知识点。希望可以帮助大家。由于本人水平有限,有什么错误和不足,希望指出。

转载于:https://juejin.im/post/5abf3658f265da23826e1bc5

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值