原稿:语雀 · 作用域插槽 · 原目录:Vue › 【原理三】模板编译 › 作用域插槽
参考链接:slot
虽然自己很少用插槽,奈何面经常有问作用域插槽相关的问题,这里探索下
普通插槽
let AppLayout = {
template: '<div class="container">' +
'<header><slot name="header"></slot></header>' +
'<main><slot>默认内容</slot></main>' +
'<footer><slot name="footer"></slot></footer>' +
'</div>'
}
let vm = new Vue({
el: '#app',
template: '<div>' +
'<app-layout>' +
'<h1 slot="header">{{title}}</h1>' +
'<p>{{msg}}</p>' +
'<p slot="footer">{{desc}}</p>' +
'</app-layout>' +
'</div>',
data() {
return {
title: '我是标题',
msg: '我是内容',
desc: '其它信息'
}
},
components: {
AppLayout
}
})
普通插槽通常用于父组件向子组件插入内容,当未显式指定插槽名时(具名插槽),默认使用 default 作为插槽名;以上的父组件编译后的渲染函数如下,可以看到插槽本身会在父组件渲染过程中调用 createElement() 生成 vnode,因此当中的 this 指向父组件,获取的数据也取自父组件
with(this){
return _c('div',
[_c('app-layout',
[_c('h1',{attrs:{"slot":"header"},slot:"header"},
[_v(_s(title))]),
_c('p',[_v(_s(msg))]),
_c('p',{attrs:{"slot":"footer"},slot:"footer"},
[_v(_s(desc))]
)
])
],
1)}
子组件渲染函数如下,对于插槽部分调用了 _t 函数,对应 renderSlot()
with(this) {
return _c('div',{
staticClass:"container"
},[
_c('header',[_t("header")],2),
_c('main',[_t("default",[_v("默认内容")])],2),
_c('footer',[_t("footer")],2)
]
)
}
/**
* Runtime helper for rendering <slot>
*/
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 }
else {
const slotNodes = this.$slots[name]
if (slotNodes) {
slotNodes._rendered = true
}
nodes = slotNodes || fallback
}
const target = props && props.slot
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
在 renderSlot() 中有 const slotNodes = this.$slots
,而 $slots 是子组件在 initRender() 中从父组件中获取到的,它是一个对象,其中每个值都是 vnodes 数组,每个 vnodes 都是在父组件执行 __render() 时生成的,这样就实现在父组件替换子组件插槽的内容
作用域插槽
作用域插槽和普通插槽的区别在于,它的数据来自于子组件中,即执行渲染函数时,this 指向的是子组件实例
当有如下代码
let Child = {
template: '<div class="child">' +
'<slot text="Hello " :msg="msg"></slot>' +
'</div>',
data() {
return {
msg: 'Vue'
}
}
}
let vm = new Vue({
el: '#app',
template: '<div>' +
'<child>' +
'<template slot-scope="props">' +
'<p>Hello from parent</p>' +
'<p>{{ props.text + props.msg}}</p>' +
'</template>' +
'</child>' +
'</div>',
components: {
Child
}
})
父组件渲染函数结果如下,可以看到,原来的父组件插槽对应的 children 数组消失了,而 data 部分多了一个对象 scopedSlots,并且执行了 _u 方法,传入了一个数组,当中每个元素是一个指定了 key 的函数组合
with(this){ return _c('div', [_c('child', {scopedSlots:_u([ { key: "default", fn: function(props) { return [ _c('p',[_v("Hello from parent")]), _c('p',[_v(_s(props.text + props.msg))]) ] } }]) } )], 1)}
_u 函数对的就是 resolveScopedSlots(),后者遍历 fns 数组,生成一个数组,数组的 key 就是插槽名称,value 就是函数,然后返回该对象
export function resolveScopedSlots ( fns: ScopedSlotsData, // see flow/vnode res?: Object): { [key: string]: Function } { res = res || {} for (let 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}
子组件的渲染函数结果如下,和普通插槽子组件相同,都执行了 _t 函数,即 renderSlot()
with(this){
return _c('div',
{staticClass:"child"},
[_t("default",null,
{text:"Hello ",msg:msg}
)],
2)}
export function renderSlot (
name: string,
fallback: ?Array<VNode>,
props: ?Object,
bindObject: ?Object
): ?Array<VNode> {
const scopedSlotFn = this.$scopedSlots[name]
let nodes
if (scopedSlotFn) {
props = props || {}
if (bindObject) {
props = extend(extend({}, bindObject), props)
}
nodes = scopedSlotFn(props) || fallback
} else {
// ...
}
const target = props && props.slot
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
再次进入 _t 函数,在作用域插槽的情况下会执行 const scopedSlotFn = this.$scopedSlots
,那么这个 this.$scopedSlots 又是在什么地方定义的呢?在子组件的渲染函数执行前,在 vm._render() 内,有这么一段逻辑
if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
}
而这个 _parentVNode.data.scopedSlots 对应的就是我们在父组件通过执行 _u 函数得到的数组,因此这里通过插槽名获取该插槽对应的 scopedSlotFn 函数,然后执行,此时是子组件渲染的过程,因此 this 指向的是子组件实例,访问的也是子组件作用域,此时执行 createElement() 生成的 vnode 自然会从子组件获取数据
小结
普通插槽: 2. 父组件模板编译后渲染函数中对设置了 slot 属性的节点生成 _c 函数,将插槽名作为 data 参数;父组件渲染阶段执行 createElement() 生成 vnodes,所以数据的作用域是父组件实例 4. 子组件模板编译后渲染函数中对 <slot> 节点生成 _t 函数;渲染的时候通过 $slots 直接拿到在父组件中生成的 vnodes
作用域插槽: 2. 父组件模板编译后渲染函数中对置了 slot-scope 属性的 <template> ,生成 scopeSlots 对象作为 data 参数,该对象调用 _u 函数;父组件渲染阶段调用 _u 方法,生成一个数组,每个数组项时一个由多个 _c 函数组合的函数 4. 子组件模板编译后渲染函数中对 <slot> 节点生成 _t 函数;渲染时执行,通过 $scopedSlots 拿到父组件渲染时生成的 scopedSlotFn,在子组件中执行,生成 vnodes,期间调用 createElement 时的 this 执行子组件实例,因此数据都来自子组件
简单地说,两种插槽的目的都是让子组件 slot 占位符生成的内容由父组件来决定,但数据的作用域会根据它们 vnodes 渲染时机不同而不同:普通插槽直接在父组件渲染时生成了插入内容的 vnodes,而作用域插槽在父组件渲染时只得到了 scopeSlotFn,作用域插槽函数的执行会在子组件渲染中