开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 12 天,点击查看活动详情
具名插槽
在实际开发过程中,往往需要灵活使用插槽进行通用组件的开发,要求组件每个模版对应子组件中的每个插槽,这时候可以使用 <slot> 中的 name 属性,称之为具名插槽
var child = {
template: `<div class="child"><slot name="header"></slot></div>`,
}
var vm = new Vue({
el: '#app',
components: {
child
},
template: `<div id="app"><child><template v-slot:header><span>头部</span></template></child></div>`,
})
模版编译的差别
父组件在编译 AST 阶段和普通节点的过程不同,具名插槽一般会在 template 模版中使用 v-slot 来指定使用哪一个插槽,在这一阶段会在编译阶段特殊处理,最终生成的 AST 树会携带 scopedSlots 用来记录具名插槽的内容,最终在 AST 中保存的插槽模版信息如下图
最终生成 render 函数如下
with(this){return _c('div',{attrs:{"id":"app"}},[_c('child',{scopedSlots:_u([{key:"header",fn:function(){return undefined},proxy:true}])})],1)}
通过上面的 render 函数可以看出,父组件的插槽内容用 _u 函数封装成数组的形式,并复制到 scopedSlots 属性中,而每一个插槽以对象形式描述, key 代表插槽名, fn 是一个返回执行结果的函数。
父组件虚拟 DOM 生成阶段
在获取到 render 函数之后,下一阶段就是生成父组件的虚拟 DOM , 其中 _u 函数的原型是 resolveScopedSlots ,该函数第一个参数就是插槽数组
export function resolveScopedSlots (
fns: ScopedSlotsData, // see flow/vnode
res?: Object,
// the following are added in 2.6
hasDynamicKeys?: boolean,
contentHashKey?: number
): { [key: string]: Function, $stable: boolean } {
res = res || { $stable: !hasDynamicKeys }
for (let i = 0; i < fns.length; i++) {
const slot = fns[i]
// fn 是数组是,需要进行递归处理
if (Array.isArray(slot)) {
resolveScopedSlots(slot, res, hasDynamicKeys)
} else if (slot) {
// marker for reverse proxying v-slot without scope on this.$slots
if (slot.proxy) {
slot.fn.proxy = true
}
res[slot.key] = slot.fn
}
}
if (contentHashKey) {
(res: any).$key = contentHashKey
}
return res
}
最终父组件的虚拟 DOM 节点的 data 属性上多了 scopedSlots 数组。回顾一下,具名插槽和普通插槽的实现上有明显的不同,普通插槽是以 componentOptions.child 的形式保留在父组件中;而具名插槽是以 scopedSlots 属性的形式存储到 data 中。
子组件渲染虚拟 DOM 过程
和普通插槽类似,子组件渲染真实节点的过程中会执行子组件的 render 函数中的 _t 方法, 也就是 renderSlot 方法
function renderSlot (
name: string,
fallback: ?Array<VNode>,
props: ?Object,
bindObject: ?Object
): ?Array<VNode> {
// 由于具名插槽的相关信息会存入 this.$scopedSlots
const scopedSlotFn = this.$scopedSlots[name]
let nodes
// 因此 scopedSlotFn 为 true ,进入该分支
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)
}
nodes = scopedSlotFn(props) || fallback
} else {
nodes = this.$slots[name] || fallback
}
const target = props && props.slot
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
从上面的代码中可以看出,对与具名插槽的核心逻辑就是 nodes = scopedSlotFn(props) || fallback , scopedSlotFn 函数就是虚拟 DOM 中保存的插槽相关的 render 函数。
到此,关于具名插槽的相关流程就已经分析完了
作用域插槽
在 Vue 开发中,我们可以使用作用域插槽让父组件访问到子组件的数据,具体用法是在子组件中以属性的方式记录数据,父组件通过 v-slot:[name]=[props] 的形式拿到子组件传递的值。 子组件在 <slot> 元素上的属性称为 插槽 Props ,
var child = {
template: `<div><slot :user="user"></div>`,
data() {
return {
user: {
firstname: 'test'
}
}
}
}
var vm = new Vue({
el: '#app',
components: {
child
},
template: `<div id="app"><child><template v-slot:default="slotProps">{{slotProps.user.firstname}}</template></child></div>`
})
父组件编译阶段
作用域插槽和具名插槽类似,区别在于 v-slot 定义了一个插槽 props 的名字,参考对于具名插槽的分析,生成 render 函数阶段 fn 函数会携带 props 参数传入,即
with(this){return _c('div',{attrs:{"id":"app"}},[_c('child',{scopedSlots:_u([{key:"default",fn:function(slotProps){return [_v(_s(slotProps.user.firstname))]}}])})],1)}
子组件渲染
在子组件编译阶段, :user=user 会以属性的形式解析,最终在 render 函数生成阶段以对象参数的形式传递给 _t 函数
with(this){return _c('div',[_t("default",null,{"user":user})],2)}
子组件渲染虚拟 DOM 阶段,根据前面分析会执行 renderSlot 函数,对于作用域插槽的处理,几种体现在函数传入的第三个参数
export function renderSlot (
name: string,
fallback: ?Array<VNode>,
props: ?Object, // 子组件传递给父组件的值
bindObject: ?Object
): ?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
props = extend(extend({}, bindObject), props)
}
nodes = scopedSlotFn(props) || fallback
} else {
nodes = this.$slots[name] || fallback
}
const target = props && props.slot
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
最终将子组件的插槽 props 作为参数传递给执行函数执行。