渲染结果:
<div class="child"><span>头部</span><span>底部</span></div>
接下来我们在普通插槽的基础上,看看源码在具名插槽实现上的区别。
父组件在编译AST
阶段和普通节点的过程不同,具名插槽一般会在template
模板中用v-slot:
来标注指定插槽,这一阶段会在编译阶段特殊处理。最终的AST
树会携带scopedSlots
用来记录具名插槽的内容
{
scopedSlots: {
footer: { ··· },
header: { ··· }
}
}
很明显,父组件的插槽内容用_u
函数封装成数组的形式,并赋值到scopedSlots
属性中,而每一个插槽以对象形式描述,key
代表插槽名,fn
是一个返回执行结果的函数。
照例进入父组件生成Vnode
阶段,其中_u
函数的原形是resolveScopedSlots
,其中第一个参数就是插槽数组。
// vnode生成阶段针对具名插槽的处理 _u (target._u = resolveScopedSlots)
function resolveScopedSlots (fns,res,hasDynamicKeys,contentHashKey) {
res = res || { $stable: !hasDynamicKeys };
for (var i = 0; i < fns.length; i++) {
var slot = fns[i];
// fn是数组需要递归处理。
if (Array.isArray(slot)) {
resolveScopedSlots(slot, res, hasDynamicKeys);
} else if (slot) {
if (slot.proxy) { // 针对proxy的处理
}
// 最终返回一个对象,对象以slotname作为属性,以fn作为值
res[slot.key] = slot.fn;
}
}
if (contentHashKey) {
(res).$key = contentHashKey;
}
return res
}
最终父组件的vnode
节点的data
属性上多了scopedSlots
数组。回顾一下,具名插槽和普通插槽实现上有明显的不同,普通插槽是以componentOptions.child
的形式保留在父组件中,而具名插槽是以scopedSlots
属性的形式存储到data
属性中。
// vnode
{
scopedSlots: [{
'header': fn,
'footer': fn
}]
}
最终子组件实例上的属性会携带父组件插槽相关的内容。
// 子组件Vnode
{
$scopedSlots: [{
'header': f,
'footer': f
}
和普通插槽类似,子组件渲染真实节点的过程会执行子render
函数中的_t
方法,这部分的源码会和普通插槽走不同的分支,其中this.$scopedSlots
根据上面分析会记录着父组件插槽内容相关的数据,所以会和普通插槽走不同的分支。而最终的核心是执行nodes = scopedSlotFn(props)
,也就是执行function(){return [_c('span',[_v("头部")])]}
,具名插槽之所以是函数的形式执行而不是直接返回结果,我们在后面揭晓。
function renderSlot (
name,
fallback, // slot插槽后备内容
props, // 子传给父的值
bindObject
){
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
// 针对具名插槽,特点是$scopedSlots有值
if (scopedSlotFn) { // scoped slot
props = props || {};
if (bindObject) {
if (!isObject(bindObject)) {
warn('slot v-bind without argument expects an Object',this);
}
props = extend(extend({}, bindObject), props);
}
// 执行时将子组件传递给父组件的值传入fn
nodes = scopedSlotFn(props) || fallback;
}···
至此子组件通过slotName
找到了对应父组件的插槽内容。