共计 4121 个字符,预计需要花费 11 分钟才能阅读完成。
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
函数简化下,其它情况先不看,这里会把 name
和fallback
传进来,
这里先说一下 name
属性,我们上面定义是
Vue.component('button-counter', {template: '<div><slot> 我是默认内容 </slot></div>'})
这里 slot
没有给 props
为name
的值,所以默认是 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
就是把 children
的name
相同的 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 地址