详解 Vue 中的插槽用法:默认插槽、具名插槽、作用域插槽
本文保留原文从默认插槽、具名插槽到作用域插槽的学习路线,并补充 Vue 3.5 的写法、插槽作用域、动态插槽名、回退内容和
defineSlots。原文中的抓取噪声、默认插槽名称拼写错误、把v-slot用在普通 HTML 元素上的错误说法以及 Vue 2/3 混用已修正。
一、什么是插槽
在项目开发中,一个组件通常需要被多个页面复用。父组件除了通过 props 给子组件传递数据、通过 emits 接收事件之外,有时还希望决定子组件某一块区域显示什么内容。
插槽(slot)就是 Vue 的内容分发机制:子组件预留一个或多个位置,父组件在使用子组件时把模板内容传入这些位置。
父组件提供内容
↓
子组件通过 <slot> 声明接收位置
↓
Vue 将父组件的内容渲染到子组件模板的指定位置
插槽内容不是简单的 HTML 字符串。父组件传入的模板会保留父组件的渲染作用域;如果父组件需要使用子组件内部的数据,应使用作用域插槽,由子组件显式传递 slot props。
原文使用 Element UI 的 Badge 和 Progress 展示组件复用场景。Element UI 主要面向 Vue 2;Vue 3 项目通常选择 Element Plus 或其他兼容 Vue 3 的组件库,插槽机制本身不受具体 UI 库影响。

下面是一个简单的父子组件关系:
<!-- Child.vue:Vue 2/3 Options API 都可以理解这段模板 -->
<template>
<div class="child">
<div>
<span>Badge 标记</span>
<div class="content">
<!-- Vue 2 + Element UI 示例 -->
<el-badge :value="12" class="item">
<el-button size="small">评论</el-button>
</el-badge>
</div>
</div>
<div>
<span>进度条</span>
<div class="content">
<el-progress :percentage="50" />
</div>
</div>
</div>
</template>
<!-- App.vue -->
<template>
<div id="app">
<Child />
</div>
</template>
<script>
import Child from './components/Child.vue'
export default {
name: 'App',
components: { Child },
}
</script>
此时 Child 内部的固定内容会正常显示。但如果父组件直接在 <Child> 标签内部写入一段内容,子组件没有声明 <slot> 时,这段内容不会出现在子组件模板预期的位置:
<Child>
<h1>欢迎大家关注小土豆</h1>
</Child>


二、默认插槽
2.1 子组件声明 <slot>
在子组件需要接收内容的位置写入 <slot />:
<!-- Child.vue -->
<template>
<div class="child">
<div>子组件原有内容</div>
<!-- 父组件没有传内容时,显示这里的回退内容 -->
<slot>我是这个 slot 里面本来的内容</slot>
</div>
</template>
父组件可以直接把内容放入子组件标签中:
<Child>
<h1>欢迎大家关注小土豆</h1>
</Child>
如果父组件提供了内容,<slot> 内的回退内容会被替换;如果没有提供内容,才显示回退内容。

2.2 显式写默认插槽
Vue 2.6+ 和 Vue 3 可以使用 v-slot,简写为 #:
<Child>
<template #default>
<h1>欢迎大家关注小土豆</h1>
</template>
</Child>
对于只有默认插槽的简单场景,直接写子节点更简洁;需要和其他具名插槽一起使用,或者需要绑定 slot props 时,使用 <template #default> 更清晰。
默认插槽的名字确实是 default,原文示例中的拼写错误需要改正:
<Child>
<template v-slot:default>
<h1>欢迎关注小土豆</h1>
</template>
</Child>
2.3 插槽内容的作用域
插槽内容由父组件提供,因此默认使用父组件的作用域:
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const parentMessage = ref('来自父组件')
</script>
<template>
<Child>
<!-- 这里读取的是 Parent 的 parentMessage -->
<span>{{ parentMessage }}</span>
</Child>
</template>
不能在这里直接读取 Child 内部未暴露的 childMessage。需要子组件传递 slot props 时,使用作用域插槽,见后文。
三、具名插槽
当组件需要多个内容分发位置时,为每个 <slot> 设置 name:
<!-- Child.vue -->
<template>
<div class="child">
<header>
<slot name="heading" />
</header>
<section>
<slot name="sub-heading" />
<div>子组件自己的 Badge/Progress 内容</div>
</section>
<footer>
<slot name="footer-text" />
</footer>
</div>
</template>
父组件使用 v-slot:name 或 #name:
<!-- App.vue,Vue 2.6+ / Vue 3 -->
<Child>
<template #heading>
<h1>Element 组件</h1>
</template>
<template #sub-heading>
<p>这里是 Element 组件的介绍</p>
</template>
<template #footer-text>
<p>出品:小土豆</p>
</template>
</Child>
未提供的具名插槽不会自动显示内容;如果希望它有默认内容,应在子组件的 <slot name="..."> 内提供回退内容。


3.1 具名插槽和默认插槽同时存在
未命名的 <slot> 接收默认插槽内容:
<template>
<div>
<slot name="header" />
<main>
<slot />
</main>
<slot name="footer" />
</div>
</template>
<Child>
<template #header>
<h2>标题</h2>
</template>
<p>这段内容进入默认插槽</p>
<template #footer>
<small>页脚</small>
</template>
</Child>
3.2 v-slot 的使用位置
v-slot 应该使用在组件标签或 <template> 上:
<Child>
<template #heading>
<h1>标题</h1>
</template>
</Child>
原文说可以把 v-slot="default" 写在任意普通 div 上并产生一层 DOM,这是不准确的。普通 HTML 元素不负责接收 Vue 组件插槽,v-slot 也不能这样使用;如果需要一层真实 DOM 包裹,直接写 <div>,再把它作为 slot 内容传入即可:
<Child>
<div class="text">
<h1>欢迎关注小土豆</h1>
</div>
</Child>
<template> 本身不会渲染成真实 DOM 节点,只有其中的内容会被插入。
3.3 动态插槽名
Vue 3 支持动态参数形式:
<script setup>
import { ref } from 'vue'
const dynamicSlotName = ref('header')
</script>
<template>
<Child>
<template #[dynamicSlotName]>
<h2>动态插槽内容</h2>
</template>
</Child>
</template>
Vue 2.6+ 也支持 v-slot:[dynamicSlotName],但要注意动态参数表达式必须能够得到合法的插槽名。
四、作用域插槽
作用域插槽解决的是:子组件提供数据,父组件决定如何渲染这部分数据。
数据流可以表示为:
子组件准备 slot props
↓
父组件通过 v-slot 接收
↓
父组件使用自己的模板渲染
这不是让父组件直接访问子组件的内部状态,而是子组件主动把指定数据作为插槽参数暴露出来。
4.1 子组件提供 slot props
<!-- Child.vue -->
<script>
export default {
name: 'Child',
data() {
return {
heading: '这里是默认的 heading',
}
},
}
</script>
<template>
<div class="child">
<slot name="heading" :headingValue="heading">
{{ heading }}
</slot>
</div>
</template>
也可以使用 Vue 3 <script setup>:
<script setup>
import { ref } from 'vue'
const heading = ref('这里是默认的 heading')
</script>
<template>
<slot name="heading" :headingValue="heading" />
</template>
在模板中,ref 会自动解包,所以传给 slot 的是当前值。
4.2 父组件接收 slot props
<Child>
<template #heading="slotProps">
<h1>Element 组件:{{ slotProps.headingValue }}</h1>
</template>
</Child>
变量名可以任意命名。也可以使用解构:
<Child>
<template #heading="{ headingValue }">
<h1>{{ headingValue }}</h1>
</template>
</Child>
原文中 slotValue 是一个对象,键名就是子组件传出的 headingValue,这个理解是正确的;只是现在通常使用 #heading="{ headingValue }" 来减少重复写法。


五、作用域插槽的应用:可定制 Card
假设有一个 Card 组件,原始版本负责渲染标题和列表:
<!-- Card.vue,Vue 2 Options API 写法 -->
<template>
<div class="card">
<h3>{{ title }}</h3>
<p v-for="item in list" :key="item.id">
{{ item.id }}.{{ item.text }}
</p>
</div>
</template>
<script>
export default {
name: 'Card',
props: {
title: String,
list: {
type: Array,
default: () => [],
},
},
}
</script>
父组件可以传入:
<Card :title="title" :list="list" />
这种 Card 能复用,但显示方式固定。如果某个页面想在标题处增加图标,或者列表项不显示 1、2、3 编号,可以把渲染权交给父组件:
<!-- Card.vue -->
<template>
<div class="card">
<h3>
<slot name="title" :titleValue="title">
{{ title }}
</slot>
</h3>
<p v-for="item in list" :key="item.id">
<slot name="text" :itemValue="item">
{{ item.id }}.{{ item.text }}
</slot>
</p>
</div>
</template>
父组件自定义标题:
<Card :title="title" :list="list">
<template #title="{ titleValue }">
<span class="icon">★</span>
{{ titleValue }}
</template>
</Card>
父组件自定义列表文本,不显示编号:
<Card :title="title" :list="list">
<template #text="{ itemValue }">
{{ itemValue.text }}
</template>
</Card>



这体现了“对扩展开放、对修改关闭”的组件设计思路:组件保留数据和布局的公共部分,父组件通过插槽定制局部视图。
六、Vue 3 中的插槽 API
6.1 在 <script setup> 中访问插槽
当组件需要根据插槽是否存在来决定是否渲染包裹元素,可以使用 useSlots:
<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
</script>
<template>
<div v-if="slots.header" class="header">
<slot name="header" />
</div>
<main>
<slot />
</main>
</template>
slots.header 表示是否提供了这个 slot 函数,不代表调用后一定有可见文本;复杂场景还要根据实际 slot 内容判断。
6.2 defineSlots
Vue 3.3+ 的 <script setup> 支持 defineSlots 编译宏,可以为插槽 props 和返回内容提供类型提示:
<script setup lang="ts">
const slots = defineSlots<{
default?: (props: { message: string }) => any
footer?: (props: { disabled: boolean }) => any
}>()
const message = '来自子组件'
</script>
<template>
<slot :message="message" />
</template>
defineSlots 只在编译时使用,不需要导入;Vue 3.5 项目可以用它改善大型组件的插槽类型体验。
6.3 作用域插槽和渲染函数
在 Vue 3 的 render function 中,插槽通常表现为函数:
import { h } from 'vue'
export default {
setup(_, { slots }) {
return () => h('section', [
slots.header?.(),
h('main', slots.default?.()),
])
},
}
带参数时:
slots.item?.({ value: item })
这也说明了为什么 slot props 是子组件传给父组件插槽函数的参数,而不是普通的父子 props 反向修改。
七、Vue 2 的旧插槽语法
Vue 2.6.0 之前常见 slot 和 slot-scope;Vue 2.6+ 推荐统一使用 v-slot,Vue 3 已移除这些旧语法。
7.1 旧默认插槽
<!-- Child.vue -->
<slot />
<!-- Parent.vue,Vue 2.5 及更早常见写法 -->
<Child>
<template slot="default">
<h1>欢迎关注小土豆</h1>
</template>
</Child>
简单默认内容也可以直接写在 <Child> 内部:
<Child>
<h1>欢迎关注小土豆</h1>
</Child>
7.2 旧具名插槽
<!-- Child.vue -->
<slot name="heading" />
<slot name="footer-text" />
<!-- Parent.vue -->
<Child>
<template slot="heading">
<h1>Element 组件</h1>
</template>
<template slot="footer-text">
<p>出品:小土豆</p>
</template>
</Child>
7.3 旧作用域插槽
<!-- Child.vue -->
<slot name="heading" :headingValue="heading">
{{ heading }}
</slot>
<!-- Parent.vue -->
<Child>
<template slot="heading" slot-scope="slotProps">
<h1>{{ slotProps.headingValue }}</h1>
</template>
</Child>
迁移到 Vue 2.6+ 或 Vue 3 时,应改为 v-slot/# 语法。



八、插槽、props 和事件如何选择
- props:父组件向子组件传递数据,适合明确的状态和配置;
- emits/事件:子组件通知父组件发生了某件事;
- 插槽:父组件定制子组件的局部视图;
- provide/inject:跨多层组件提供依赖,避免逐层透传;
- Pinia:多个页面或不相邻组件共享和管理应用状态。
插槽并不是所有通信问题的替代方案。一个组件如果通过大量 slot props 传递复杂状态,可能应该拆分组件边界,或者使用 composable、provide/inject、Pinia 等更合适的机制。
总结
- 插槽是 Vue 的内容分发机制,子组件通过
<slot>声明接收位置; - 默认插槽使用
<slot />,具名插槽使用name与#name/v-slot:name; - 插槽内容默认在父组件作用域中编译;
- 作用域插槽由子组件传出 slot props,父组件决定如何渲染;
<template #name>不会生成额外 DOM,普通 HTML 元素不能随意使用v-slot;- Vue 3 支持动态插槽名、
useSlots和defineSlots; - Vue 2 的
slot/slot-scope是历史语法,Vue 3 应使用v-slot/#; - 组件设计应根据需求选择 props、事件、插槽、provide/inject 或 Pinia。
参考资料
- Vue 3:Slots
- Vue 3:Composition API Helpers(
useSlots) - Vue 3:
defineSlots - Vue 3:Provide / Inject
- Vue 2:Slots
- Vue 3 迁移指南:Slots Unification
原文作者:小土豆 biubiubiu。原文关于默认插槽、具名插槽、作用域插槽、Card 组件扩展和旧语法的主线予以保留;抓取标记、失效图片、默认插槽名称拼写、错误的普通元素 v-slot 示例以及 Vue 2/3 语法混用已整理或修正。