插槽
本页假设你已经阅读过 组件基础。如果你是组件新手,请先阅读那一节。
插槽内容与出口
我们已经了解到,组件可以接收 props,而 props 可以是任意类型的 JavaScript 值。但模板内容呢?在某些情况下,我们可能希望将一个模板片段传递给子组件,并让子组件在自己的模板中渲染这个片段。
例如,我们可能有一个 <FancyButton> 组件,它支持如下用法:
template
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton><FancyButton> 的模板如下所示:
template
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button><slot> 元素是一个 插槽出口,它表示父组件提供的 插槽内容 应该被渲染到哪里。

最终渲染出的 DOM:
html
<button class="fancy-btn">Click me!</button>通过插槽,<FancyButton> 负责渲染外层的 <button>(以及它华丽的样式),而内部内容则由父组件提供。
理解插槽的另一种方式,是将它们与 JavaScript 函数进行比较:
js
// 父组件传递插槽内容
FancyButton('Click me!')
// FancyButton 在自己的模板中渲染插槽内容
function FancyButton(slotContent) {
return `<button class="fancy-btn">
${slotContent}
</button>`
}插槽内容不只是限于文本。它可以是任何有效的模板内容。例如,我们可以传入多个元素,甚至其他组件:
template
<FancyButton>
<span style="color:red">Click me!</span>
<AwesomeIcon name="plus" />
</FancyButton>使用插槽后,我们的 <FancyButton> 更加灵活且可复用。现在我们可以在不同地方使用它,并传入不同的内部内容,但都保持同样华丽的样式。
Vue 组件的插槽机制受到了 原生 Web Components 的 <slot> 元素 的启发,但它还具备一些我们后面会看到的额外能力。
渲染作用域
插槽内容可以访问父组件的数据作用域,因为它是在父组件中定义的。例如:
template
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>这里两个 {{ message }} 插值都会渲染出相同的内容。
插槽内容不能访问子组件的数据。Vue 模板中的表达式只能访问其定义所在的作用域,这与 JavaScript 的词法作用域一致。换句话说:
父模板中的表达式只能访问父作用域;子模板中的表达式只能访问子作用域。
备用内容
有些情况下,为插槽指定备用内容(即默认内容)很有用,这些内容只会在没有提供任何内容时才被渲染。例如,在一个 <SubmitButton> 组件中:
template
<button type="submit">
<slot></slot>
</button>如果父组件没有为插槽提供任何内容,我们可能希望在 <button> 内部渲染文本 "Submit"。要将 "Submit" 设为备用内容,可以把它放在 <slot> 标签之间:
template
<button type="submit">
<slot>
Submit <!-- 备用内容 -->
</slot>
</button>现在,当我们在父组件中使用 <SubmitButton> 且没有为插槽提供内容时:
template
<SubmitButton />这将渲染备用内容 "Submit":
html
<button type="submit">Submit</button>但如果我们提供内容:
template
<SubmitButton>Save</SubmitButton>那么渲染的将是提供的内容:
html
<button type="submit">Save</button>具名插槽
有时在单个组件中拥有多个插槽出口会很有用。例如,在 <BaseLayout> 组件中有如下模板:
template
<div class="container">
<header>
<!-- 我们希望这里放入 header 内容 -->
</header>
<main>
<!-- 我们希望这里放入 main 内容 -->
</main>
<footer>
<!-- 我们希望这里放入 footer 内容 -->
</footer>
</div>在这种情况下,<slot> 元素有一个特殊属性 name,可以用来给不同的插槽分配唯一的 ID,这样你就可以决定内容应该被渲染到哪里:
template
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>没有 name 的 <slot> 出口会隐式地拥有 "default" 这个名称。
在使用 <BaseLayout> 的父组件中,我们需要一种方式来传递多个插槽片段,每个片段都对应不同的插槽出口。这就是具名插槽的作用。
要传递具名插槽,我们需要使用带有 v-slot 指令的 <template> 元素,然后将插槽名称作为参数传给 v-slot:
template
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容 -->
</template>
</BaseLayout>v-slot 还有一个专用简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。可以把它理解为“将这个模板片段渲染到子组件的 ‘header’ 插槽中”。

下面是使用简写语法向 <BaseLayout> 传递三个插槽内容的代码:
template
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>当一个组件同时接受默认插槽和具名插槽时,所有顶层的非 <template> 节点都会被隐式视为默认插槽的内容。因此,上面的内容也可以写成:
template
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<!-- 隐式默认插槽 -->
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>现在,<template> 元素中的所有内容都会被传递到对应的插槽中。最终渲染出的 HTML 将会是:
html
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>同样地,使用 JavaScript 函数类比也有助于更好地理解具名插槽:
js
// 使用不同名称传递多个插槽片段
BaseLayout({
header: `...`,
default: `...`,
footer: `...`
})
// <BaseLayout> 在不同位置渲染它们
function BaseLayout(slots) {
return `<div class="container">
<header>${slots.header}</header>
<main>${slots.default}</main>
<footer>${slots.footer}</footer>
</div>`
}条件插槽
有时候,你希望根据内容是否传入了某个插槽来渲染一些内容。
你可以结合使用 $slots 属性和 v-if 来实现这一点。
在下面的示例中,我们定义了一个 Card 组件,它有三个条件插槽:header、footer 以及 default。 当 header / footer / default 的内容存在时,我们希望用额外的样式包裹它们:
template
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div v-if="$slots.default" class="card-content">
<slot />
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>动态插槽名
动态指令参数 同样适用于 v-slot,这允许定义动态插槽名:
template
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 使用简写 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>请注意,该表达式需要遵循动态指令参数的 语法限制。
作用域插槽
正如在 渲染作用域 中所讨论的,插槽内容无法访问子组件中的状态。
不过,在某些情况下,插槽内容同时使用父作用域和子作用域中的数据会很有用。为此,我们需要一种方式,让子组件在渲染插槽时向插槽传递数据。
事实上,我们完全可以这么做——就像向组件传递 props 一样,我们可以向插槽出口传递属性:
template
<!-- <MyComponent> 模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>接收插槽 props 的方式,在单个默认插槽和具名插槽之间会略有不同。我们先展示如何通过直接在子组件标签上使用 v-slot 来接收单个默认插槽的 props:
template
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>子组件传给插槽的 props 会作为对应 v-slot 指令的值可用,并且可以在插槽内部通过表达式访问。
你可以把作用域插槽看作传入子组件的一个函数。然后子组件再调用它,并把 props 作为参数传入:
js
MyComponent({
// 传入默认插槽,但形式是一个函数
default: (slotProps) => {
return `${slotProps.text} ${slotProps.count}`
}
})
function MyComponent(slots) {
const greetingMessage = 'hello'
return `<div>${
// 使用 props 调用插槽函数!
slots.default({ text: greetingMessage, count: 1 })
}</div>`
}实际上,这与作用域插槽的编译方式非常接近,也与你在手写 渲染函数 中使用作用域插槽的方式一致。
注意 v-slot="slotProps" 与插槽函数签名的对应关系。和函数参数一样,我们也可以在 v-slot 中使用解构:
template
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>具名作用域插槽
具名作用域插槽的工作方式类似——插槽 props 可以作为 v-slot 指令的值来访问:v-slot:name="slotProps"。使用简写时,看起来像这样:
template
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>向具名插槽传递 props:
template
<slot name="header" message="hello"></slot>请注意,插槽的 name 不会包含在 props 中,因为它是保留属性——因此得到的 headerProps 会是 { message: 'hello' }。
如果你把具名插槽和默认作用域插槽混用,就需要为默认插槽显式使用 <template> 标签。尝试直接把 v-slot 指令放在组件上会导致编译错误。这样做是为了避免默认插槽 props 的作用域产生任何歧义。例如:
template
<!-- <MyComponent> 模板 -->
<div>
<slot :message="hello"></slot>
<slot name="footer" />
</div>template
<!-- 此模板不会编译 -->
<MyComponent v-slot="{ message }">
<p>{{ message }}</p>
<template #footer>
<!-- message 属于默认插槽,在这里不可用 -->
<p>{{ message }}</p>
</template>
</MyComponent>为默认插槽使用显式的 <template> 标签,有助于清楚地表明 message prop 在其他插槽内部不可用:
template
<MyComponent>
<!-- 使用显式默认插槽 -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>
<template #footer>
<p>这里有一些联系信息</p>
</template>
</MyComponent>精彩列表示例
你可能会想,作用域插槽的一个好用例会是什么。这里有一个例子:设想一个 <FancyList> 组件,它会渲染一个项目列表——它可以封装远程数据加载、使用数据展示列表,甚至分页或无限滚动等高级功能。不过,我们希望它在每个项目的外观上保持灵活,并将每个项目的样式交给使用它的父组件来决定。因此,期望的用法可能如下:
template
<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p>by {{ username }} | {{ likes }} likes</p>
</div>
</template>
</FancyList>在 <FancyList> 内部,我们可以用不同的项目数据多次渲染同一个 <slot>(注意这里我们使用 v-bind 把一个对象作为插槽 props 传入):
template
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>无渲染组件
上面讨论的 <FancyList> 用例同时封装了可复用的逻辑(数据获取、分页等)和视觉输出,同时又通过作用域插槽将部分视觉输出委托给消费组件。
如果我们把这个概念再向前推进一点,就可以得到一种只封装逻辑、而自身不渲染任何内容的组件——视觉输出会通过作用域插槽完全委托给消费组件。我们把这种类型的组件称为 无渲染组件。
一个无渲染组件的例子,是封装当前鼠标位置追踪逻辑的组件:
template
<MouseTracker v-slot="{ x, y }">
鼠标位置:{{ x }}, {{ y }}
</MouseTracker>虽然这是一种有趣的模式,但无渲染组件所能实现的大多数内容,都可以通过 Composition API 更高效地实现,而不会产生额外的组件嵌套开销。后面我们会看到如何把同样的鼠标追踪功能实现为一个 可组合函数。
不过,在需要同时封装逻辑并且组合视觉输出的场景中,作用域插槽仍然很有用,例如上面的 <FancyList> 示例。