顾名思义,组件的插槽指组件会预留一个槽位,该槽位具体要渲 染的内容由用户插入,如下面给出的 MyComponent 组件的模板所 示:
<template>
<header>
<slot name="header" />
</header>
<div>
<slot name="body" />
</div>
<footer>
<slot name="footer" />
</footer>
</template>
当在父组件中使用 组件时,可以根据插槽的名 字来插入自定义的内容:
<MyComponent>
<template #header>
<h1>我是标题</h1>
</template>
<template #body>
<section>我是内容</section>
</template>
<template #footer>
<p>我是注脚</p>
</template>
</MyComponent>
上面这段父组件的模板会被编译成如下渲染函数:
// 父组件的渲染函数
function render() {
return {
type: MyComponent,
// 组件的 children 会被编译成一个对象
children: {
header() {
return { type: "h1", children: "我是标题" }
},
body() {
return { type: "section", children: "我是内容" }
},
footer() {
return { type: "p", children: "我是注脚" }
},
},
}
}
可以看到,组件模板中的插槽内容会被编译为插槽函数,而插槽 函数的返回值就是具体的插槽内容。组件 MyComponent 的模板则会 被编译为如下渲染函数:
// MyComponent 组件模板的编译结果
function render() {
return [
{
type: "header",
children: [this.$slots.header()],
},
{
type: "body",
children: [this.$slots.body()],
},
{
type: "footer",
children: [this.$slots.footer()],
},
]
}
可以看到,渲染插槽内容的过程,就是调用插槽函数并渲染由其 返回的内容的过程。这与 React 中 render props 的概念非常相似。 在运行时的实现上,插槽则依赖于 setupContext 中的 slots 对象,如下面的代码所示:
function mountComponent(vnode, container, anchor) {
// 省略部分代码
// 直接使用编译好的 vnode.children 对象作为 slots 对象即可
const slots = vnode.children || {}
// 将 slots 对象添加到 setupContext 中
const setupContext = { attrs, emit, slots }
}
可以看到,最基本的 slots 的实现非常简单。只需要将编译好的 vnode.children 作为 slots 对象,然后将 slots 对象添加到 setupContext 对象中。为了在 render 函数内和生命周期钩子函数 内能够通过 this.slots 属性,如下面的代码所示:
function mountComponent(vnode, container, anchor) {
// 省略部分代码
const slots = vnode.children || {}
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
// 将插槽添加到组件实例上
slots,
}
// 省略部分代码
const renderContext = new Proxy(instance, {
get(t, k, r) {
const { state, props, slots } = t
// 当 k 的值为 $slots 时,直接返回组件实例上的 slots
if (k === "$slots") return slots
// 省略部分代码
},
set(t, k, v, r) {
// 省略部分代码
},
})
// 省略部分代码
}
我们对渲染上下文 renderContext 代理对象的 get 拦截函数做 了特殊处理,当读取的键是 slots 来访问插槽内容 了。