Vue源码解析-了解vue插槽slot篇

框架

浏览数:264

2019-6-22

1.从简单的匿名插槽开始

Vue.component('button-counter', {
  template: '<div> <slot>我是默认内容</slot></div>'
})
new Vue({
    el: '#app',
    template: '<button-counter><span>我是slot传入内容</span></button-counter>'
})

这里先注册一个button-counter组件,然后使用它,上面渲染出来的结果是 我是slot内容

原理解析:

1.这里直接看到组件button-counter编译后的render函数,就不详细从template模板编译说起,直接看最后的render函数:

(function anonymous(
) {
with(this){return _c('div',[_t("default",[_v("我是默认内容")])],2)}
})

这里_v就是创建普通文本节点,主要看_t函数,这里_t也就是renderSlot函数的简写

function installRenderHelpers (target) {
  ...
  target._t = renderSlot;
  ...
}
function renderSlot (
  name,
  fallback,
  props,
  bindObject
) {
  var scopedSlotFn = this.$scopedSlots[name];
  var nodes;
  nodes = scopedSlotFn(props) || fallback;
  return nodes;
}

这里把函数renderSlot函数简化下,其它情况先不看,这里会把namefallback传进来,
这里先说一下name属性,我们上面定义是

Vue.component('button-counter', {
  template: '<div><slot>我是默认内容</slot></div>'
})

这里slot没有给propsname的值,所以默认是default,我们可以给它一个name值,例如

Vue.component('button-counter', {
  template: '<div><slot name="header">我是默认内容</slot></div>'
})

那么使用时

new Vue({
  el: '#app',
  template: '<button-counter><span slot="header">我是slot传入内容</span></button-counter>'
})

但是上面的用法slot在2.6.0版本已经废弃,提供了新的v-slot代替,但是你仍然可以这么写,源码中依然保存对slot的兼容处理,我们看下用v-slot的写法以及需要注意的地方

new Vue({
  el: '#app',
  template: '<button-counter><template v-slot:header><span>我是slot传入内容</span></template></button-counter>'
})

注意这里v-slot需要使用在template,不可再跟上面一样直接作用于span标签上面

回到上面说的renderSlot函数,name这里是默认值default,第二个参数fallback就是我们在组件中的slot节点的默认值

var scopedSlotFn = this.$scopedSlots[name];
var nodes;
nodes = scopedSlotFn(props) || fallback;
return nodes;

如果this.$scopredSlots存在该name的值,则调用该返回函数生成一个nodes节点返回,否则返回fallback(即默认值),所以到这里我们知道为什么在自定义组件中有传入子元素就渲染子元素,没有就使用默认插槽里面的值了,这里涉及到this.$scopredSlots这个变量,我们接下来看下这个值,我们首先看下vm.$slots
当组件在执行initRender函数时

function initRender (vm) {
  ...
  vm.$slots = resolveSlots(options._renderChildren, renderContext);
  ...
}

resolveSlots函数会对children节点做归类和过滤处理,返回slots

function resolveSlots (
    children,
    context
  ) {
    if (!children || !children.length) {
      return {}
    }
    var slots = {};
    for (var i = 0, l = children.length; i < l; i++) {
      var child = children[i];
      var data = child.data;
      // remove slot attribute if the node is resolved as a Vue slot node
      if (data && data.attrs && data.attrs.slot) {
        delete data.attrs.slot;
      }
      // named slots should only be respected if the vnode was rendered in the
      // same context.
      if ((child.context === context || child.fnContext === context) &&
        data && data.slot != null
      ) {
        // 如果slot存在(slot="header") 则拿对应的值作为key
        var name = data.slot;
        var slot = (slots[name] || (slots[name] = []));
        // 如果是tempalte元素 则把template的children添加进数组中,这也就是为什么你写的template标签并不会渲染成另一个标签到页面
        if (child.tag === 'template') {
          slot.push.apply(slot, child.children || []);
        } else {
          slot.push(child);
        }
      } else {
        // 如果没有就默认是default
        (slots.default || (slots.default = [])).push(child);
      }
    }
    // ignore slots that contains only whitespace
    for (var name$1 in slots) {
      if (slots[name$1].every(isWhitespace)) {
        delete slots[name$1];
      }
    }
    return slots
  }

这里的_renderChildren就是把childrenname相同的VNodes节点归类放到一个数组中,最终返回了一个对象,key为对应的name,值是包含该name对应的节点数组

接下来的_render函数的时候,通过normalizeScopedSlots得到vm.$scopedSlots

vm.$scopedSlots = normalizeScopedSlots(
  _parentVnode.data.scopedSlots,
  vm.$slots,
  vm.$scopedSlots
);

作用域插槽

有时我们需要获取子组件的一些内部数据,比如获取下面msg的值,但是当前执行的作用域是在父组件的实例上,通过this.msg是获取不到子组件里面的值,我们需要在<slot>元素上动态绑定一个msg对象属性,然后在父组件可以通过下面方式来获取

Vue.component('button-counter', {
  data () {
    return {
        msg: 'hi'
    }
 },
  template: '<div><slot :msg="msg">我是默认内容</slot></div>'
})
new Vue({
  el: '#app',
  template: '<button-counter><template scope="msg"><span>{{props.msg}}</span></template></button-counter>'
})

这是2.5以前的写法,2.5以后采用slot-scope

new Vue({
  el: '#app',
  template: '<button-counter><template slot-scope="msg"><span>{{props.msg}}</span></template></button-counter>'
})

2.6以后废弃上面两种写法,采用v-slot

new Vue({
  el: '#app',
  template: '<button-counter><template v-slot="msg"><span>{{props.msg}}</span></template></button-counter>'
})

这里能够拿到msg是因为在renderSlot的时候 执行会传入props,可以看到编译后的render函数_t的第三个参数就是props

(function anonymous(
) {
with(this){return _c('div',[_t("default",[_v("我是默认的")],{"msg":msg})],2)}
})
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
nodes = scopedSlotFn(props) || fallback;
return nodes;

render函数可以看到传进去的props后就能够拿到msg的值了

(function anonymous(
) {
with(this){return _c('button-counter',{scopedSlots:_u([{key:"default",fn:function(props){return [_c('span',[_v(_s(props.msg))])]}}])})}
})

最后

这里只是简单描述了几个关键点,插槽作用域还有支持解构赋值,支持动态插槽名称等写法,还有很多的细节处理需要更加深入去了解源码。

如果觉得对你有帮忙,麻烦点个star哈
github地址

作者:zwStar