我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
建议先复习一下插槽的用法,官方文档点击这里。
今天和大家讲讲 Vue 中的插槽功能,主要从普通插槽、后备内容、具名插槽、作用域插槽这四个知识点进行解析。
1,普通插槽
对应文档点击这里。
首先从插槽最简单的用法讲起,实现功能的重点是子组件的实例和子组件的 render 函数,我们以下面这个简单的例子进行解析。
Vue.component('hello-world', {
template: `
<div class="hello_world">
<h2>我是 HelloWorld 组件</h2>
<slot></slot>
</div>
`,
methods: {}
})
new Vue({
el: '#app',
data() {
return {}
},
methods: {},
template: `
<div class="app_container">
<h1>我是APP</h1>
<hello-world>
<h3>我是 slot</h3>
</hello-world>
</div>
`
})
该例子渲染的页面如下所示:
1-1, 父组件的 vnode
父组件生成的 vnode 如下所示,该 vnode 节点有三个子节点,第三个子节点就是 hello-world 组件的 vnode 节点。在 Vue 中,每一个使用的组件都会有一个相对应的 Vue 实例,在目前的阶段,该组件的实例还没有被创建出来,所以 vnode.componentInstance 的值是 undefined。vnode.componentOptions 对象中的数据用于创建该组件的实例,可以发现在 vnode.componentOptions 对象中,有一个 children 属性,该属性保存着 hello-world 组件的子节点,并且该子节点已经被编译成了 vnode。
1-2,初始化子组件实例的 $slots 属性
在 1-1 小节中,父组件的 vnode 已经创建出来了,所以之后会进行父组件的 patch 操作,在 patch 到第三个子节点的时候,发现该 vnode 是一个组件节点,此时会进行子组件 vnode 的实例化操作,这部分的代码看 src/core/instance/init.js。
Vue.prototype._init = function (options?: Object) {
// vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
const vm: Component = this
// 初始化与插槽有关的内容
initRender(vm)
}
在 _init 方法中,与插槽有关的代码是 initRender(vm),我们看下 initRender() 函数。
export function initRender (vm: Component) {
// 对 options._renderChildren 进行解析,并将解析的结果赋值到 vm.$slots
vm.$slots = resolveSlots(options._renderChildren, renderContext)
}
在我们的例子中,vm.$slots 的值为:
vm.$slots = {
default: [{
// h3 vnode 节点
tag: "h3",
children: [{
// h3 vnode 节点下面的文本子节点
text: "我是 slot"
}]
}]
}
1-3,子组件执行 render 函数,生成子组件的 vnode
子组件的 render 函数如下所示:
with(this){
return _c(
'div',
{staticClass:"hello_world"},
[
_c('h2',[_v("我是 HelloWorld 组件")]),
_v(" "),
_t("default")
],
2
)
}
在子组件的 render 函数中,与 <slot></slot> 模板字符串有关的代码字符串是 _t("default"),_t 是 renderSlot 方法的别名,我们看 renderSlot 函数。
export function installRenderHelpers (target: any) {
......
target._t = renderSlot
......
}
在 renderSlot 函数中,通过 "default" slot 名称从子组件实例的 $slots 属性中获取 <hello-world></hello-world> 的子节点数组。
export function renderSlot (
name: string,
fallback: ?Array<VNode>,
props: ?Object,
bindObject: ?Object
): ?Array<VNode> {
const scopedSlotFn = this.$scopedSlots[name]
if (scopedSlotFn) { // scoped slot
......
} else {
// 代码执行到这里
// 从 this.$slots 对象中获取指定插槽的数组,在这里 name == "default"
const slotNodes = this.$slots[name]
// warn duplicate slot usage
if (slotNodes && process.env.NODE_ENV !== 'production') {
slotNodes._rendered && warn(
`Duplicate presence of slot "${name}" found in the same render tree ` +
`- this will likely cause render errors.`,
this
)
slotNodes._rendered = true
}
// 将 slotNodes 返回
return slotNodes || fallback
}
}
执行子组件 render 函数最终生成的 vnode 如下所示:
可以发现,子组件中的 <slot></slot> 已经变成了 <h3>我是 slot</h3>。
子节点的 vnode 渲染到页面上,最终的效果如下所示:
2,后备内容
对应文档点击这里。
实现该功能的重点主要看子组件的 render 函数。
我们以下面的代码为例。
Vue.component('hello-world', {
template: `
<div class="hello_world">
<h2>我是 HelloWorld 组件</h2>
<slot>
<h3>slot 默认内容</h3>
</slot>
</div>
`,
methods: {}
})
new Vue({
el: '#app',
data() {
return {}
},
methods: {},
template: `
<div class="app_container">
<h1>我是APP</h1>
<hello-world></hello-world>
</div>
`
})
子组件的模板字符串中,"<slot></slot>" 节点的下面有 "<h3>slot 默认内容</h3>" 子节点,这个 h3 节点是这个 slot 节点的后备内容。
2-1,子节点的抽象语法树
该子节点的抽象语法树如下所示:
2-2,子节点的代码字符串
子节点的代码字符串如下所示:
with(this){
return _c(
'div',
{staticClass:"hello_world"},
[
_c('h2',[_v("我是 HelloWorld 组件")])
,_v(" "),
_t(
"default",
[_c('h3',[_v("slot 默认内容")])]
)
],
2
)
}
可以发现 _t 函数有了第二个参数 [_c('h3',[_v("slot 默认内容")])],这正好对应 <slot> 节点的后备内容,接下来看 renderSlot 函数。
2-3,renderSlot
export function renderSlot (
name: string,
fallback: ?Array<VNode>,
props: ?Object,
bindObject: ?Object
): ?Array<VNode> {
const scopedSlotFn = this.$scopedSlots[name]
if (scopedSlotFn) { // scoped slot
......
} else {
// 从 this.$slots 对象中获取指定插槽的数组,在这里 name == "default"
const slotNodes = this.$slots[name]
// warn duplicate slot usage
if (slotNodes && process.env.NODE_ENV !== 'production') {
slotNodes._rendered && warn(
`Duplicate presence of slot "${name}" found in the same render tree ` +
`- this will likely cause render errors.`,
this
)
slotNodes._rendered = true
}
// 将 slotNodes 返回,如果 slotNodes 不存在的话,则返回 fallback
// fallback 是 VNode 节点的数组,是当前处理 <slot> 的后备内容
return slotNodes || fallback
}
}
renderSlot 函数的最后一行可以发现,如果 slotNodes 不存在的话,则返回 fallback,这个 fallback 就是 slot 后备内容的 vnode。
2-4,子组件的 vnode
子组件最终生成的 vnode 如下所示:
页面渲染的效果见下图:
3,具名插槽
对应的文档点击这里。
如果已经理解了普通插槽的话,那理解具名插槽也就非常简单了,它们之间并没有什么本质的区别,我们直接以一个简单的例子进行讲解。
Vue.component('hello-world', {
template: `
<div class="hello_world">
<h2>我是 HelloWorld 组件</h2>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
`,
methods: {}
})
new Vue({
el: '#app',
data() {
return {}
},
methods: {},
template: `
<div class="app_container">
<h1>我是APP</h1>
<hello-world>
<template v-slot:header>
<h3>header slot</h3>
</template>
<template v-slot:default>
<h4>default slot</h4>
</template>
<template v-slot:footer>
<h5>footer slot</h5>
</template>
</hello-world>
</div>
`
})
该例子渲染的页面如下所示:
3-1,子组件实例的 $slots 属性
可以发现,子组件实例的 $slots 属性是一个对象,并且该对象里面有三个属性,分别对应默认插槽的内容、header插槽的内容、footer插槽的内容。在这里,和第一小节的普通插槽并没有什么本质的区别,只不过 $slots 属性对象中的内容多了两条而已。
3-2,子组件的 render 函数
子组件的 render 函数如下所示:
with(this){
return _c(
'div',
{staticClass:"hello_world"},
[
_c('h2',[_v("我是 HelloWorld 组件")]),
_v(" "),
_t("header"),
_v(" "),
_t("default"),
_v(" "),
_t("footer")
],
2
)
}
子组件模板中的三个 slot。
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
分别对应。
_t("header"),
_t("default"),
_t("footer")
_t 是 renderSlot 函数的别名,借助该函数,可以从 vm.$slots 对象中获取指定 slot 应该渲染的 vnode。
3-3,子组件最终的 vnode
在最终得到的 vnode 中,可以发现,子组件模板中的三个 <slot></slot> 已经被转换成了 <h3></h3>、<h4></h4>、<h5></h5>。接下来,进行 patch 操作,渲染到页面上即可。
4,作用域插槽
对应的文档点击这里。
上面的插槽都有一个共同的特点,就是组件子节点的 vnode 都是在父组件 render 函数执行时生成的。在这一点上,作用域插槽和它们完全不同,在父组件 render 函数执行获取的 vnode 中,使用了作用域插槽的组件子节点被编译成了一个函数,保存在子组件 vnode 节点的 data.scopedSlots 对象中。当子组件的 Vue 实例创建出来后,这个函数被保存在 vm.$scopedSlots 对象中,我们以一个例子进行讲解,就很好懂了,例子代码如下:
Vue.component('hello-world', {
template: `
<div class="hello_world">
<h2>我是 HelloWorld 组件</h2>
<slot v-bind:user="user"></slot>
</div>
`,
data() {
return {
user: {
name: 'tom'
}
}
},
methods: {}
})
new Vue({
el: '#app',
data() {
return {}
},
methods: {},
template: `
<div class="app_container">
<h1>我是APP</h1>
<hello-world>
<template v-slot:default="slotProps">
<h3>{{slotProps.user.name}}</h3>
</template>
</hello-world>
</div>
`
})
该例子渲染出来的页面如下所示:
4-1,子组件的 vue 实例以及作用域插槽节点编译成的函数
父组件的 vnode 生成之后,会进行 patch 操作,patch 处理到子节点 vnode 时,会进行子组件实例的初始化,在我们这个例子中,子组件的实例如下所示:
在这个子组件的实例中,与本节有关的是 $scopedSlots 属性,该属性是一个对象,对象的 key 是插槽的名称,对象的 value 是一个函数,我们看看这个函数的函数体。
function(slotProps){
return [
_c(
'h3',
[_v(_s(slotProps.user.name))]
)
]
}
该函数有一个参数,参数的名称是 "slotProps",该名称取自 <template v-slot:default="slotProps">,这个函数的作用是创建并返回 h3 vnode 节点,并且 h3 节点中的文本取自 slotProps.user.name。
好了,至此我们知道了使用作用域插槽的节点会被编译成一个函数,并且这个函数会被保存到 vm.$scopedSlots 属性中,接下来看子组件的 render 函数。
4-2,子组件的 render 函数
本例子中,子组件的 render 函数如下所示:
with(this){
return _c(
'div',
{staticClass:"hello_world"},
[
_c('h2',[_v("我是 HelloWorld 组件")]),
_v(" "),
_t("default",null,{"user":user})
],
2
)
}
与作用域插槽有关的部分是:
_t("default",null,{"user":user})
接下来看 renderSlot 函数。
4-3,renderSlot
如果是作用域插槽的话,会执行到 if(){} 分支,该分支很简单,将 props 作为参数执行 scopedSlotFn 函数,并将该函数的返回值 return 出去即可。
export function renderSlot (
name: string,
fallback: ?Array<VNode>,
props: ?Object,
bindObject: ?Object
): ?Array<VNode> {
// 从 vm.$scopedSlots 中获取指定插槽对应的函数,在这里 name = "default"
const scopedSlotFn = this.$scopedSlots[name]
if (scopedSlotFn) { // scoped slot
// 作用域插槽 代码执行到这里
props = props || {}
// 将 props 作为参数执行 scopedSlotFn 函数,并将函数的返回值 return 出去
// scopedSlotFn 函数的作用是创建并返回作用域插槽中节点的 vnode。
return scopedSlotFn(props) || fallback
} else {
......
}
}
4-4,子节点最终的 vnode
子节点最终的 vnode 如下所示:
可以看到子组件中的 <slot v-bind:user="user"></slot> 最终被转换成了 <h3>tom</h3>。