useSlots到底是什么?和选项式的$slots有啥区别?
在Vue3的组件开发圈子里,“useSlots怎么用”“它能解决啥问题”这类疑问经常出现,尤其是刚从选项式API转到组合式API的同学,面对useSlots总有点摸不着头脑,今天咱们就围绕useSlots,从基础概念到实战场景,把常见问题一个个说清楚,帮你把这个API吃透~
简单说,useSlots是Vue3组合式API里专门用来“访问组件收到的插槽内容”的工具。
在Vue2的选项式API里,我们用this.$slots
访问插槽,比如判断有没有默认插槽、具名插槽,但组合式API强调逻辑拆分,把和插槽相关的操作放到setup
里,这时候就需要useSlots这个API来替代this.$slots
。
举个直观例子:父组件给子组件传了默认插槽和名为header
的具名插槽,子组件想在JS里判断“父有没有传header插槽”,这时候用useSlots就能拿到这些信息——调用useSlots后会得到一个对象,键是插槽名(默认插槽叫default
,具名插槽就是你定义的名字),值是对应的插槽渲染函数。
useSlots的基础用法是怎样的?得掌握哪些关键点?
先看最基础的语法流程:
- 导入API:在组件的
setup
函数里,先导入useSlots →import { useSlots } from 'vue';
- 调用获取插槽对象:
const slots = useSlots();
这里的slots是个对象,存储着所有传入的插槽。 - 访问具体插槽:比如判断默认插槽是否存在 →
slots.default
;判断具名插槽header
是否存在 →slots.header
。 - 渲染插槽:因为插槽本质是渲染函数,所以要用函数调用的方式渲染,比如
slots.default()
才能生成对应的VNode(虚拟DOM节点)。
举个实际代码例子(子组件内部):
import { useSlots } from 'vue'
export default {
setup() {
const slots = useSlots()
// 判断header插槽是否存在
const hasHeader = !!slots.header
// 判断默认插槽是否存在
const hasDefault = !!slots.default
return { slots, hasHeader, hasDefault }
}
}
模板里配合渲染:
<div class="container">
<!-- 具名插槽header的渲染 -->
<header v-if="hasHeader">{{ slots.header() }}</header>
<!-- 默认插槽的渲染 -->
<main v-if="hasDefault">{{ slots.default() }}</main>
<!-- 没有插槽时显示默认内容 -->
<p v-else>这里还没内容,快给我传插槽呀~</p>
</div>
关键点总结:要记住插槽是函数,必须调用才能渲染;useSlots返回的对象里,键和插槽名一一对应(默认插槽是default,具名插槽是你写的name)。
useSlots能解决哪些实际开发里的痛点?
很多同学觉得“插槽我用模板里的<slot>也能写默认内容,为啥还要用useSlots?” 这就得说到useSlots在JS逻辑层处理插槽的优势,典型场景有这三类:
精准判断插槽是否存在,控制UI细节
比如做一个弹窗组件,footer区域支持插槽,但如果父组件没传footer插槽,子组件要显示“确认/取消”默认按钮,这时候用useSlots判断:
const slots = useSlots()
// 模板中:
<footer class="dialog-footer">
<slot name="footer">
<button @click="confirm">确认</button>
<button @click="cancel">取消</button>
</slot>
</footer>
但如果需求更复杂——“如果父传了footer插槽,就隐藏默认的分隔线;没传就显示”,这时候就得在JS里判断:
const slots = useSlots()
const showDivider = computed(() => !slots.footer)
// 模板中根据showDivider控制分隔线显示
<div class="divider" v-if="showDivider"></div>
这种“插槽存在性影响其他UI元素”的场景,useSlots能在JS里灵活处理。
动态处理多个具名插槽,实现复杂组件逻辑
比如封装一个表格组件,列内容用具名插槽#column
传递,而且父组件可能传1个或多个column插槽,这时候用useSlots遍历处理:
const slots = useSlots()
// 假设每个column插槽对应表格一列
const columns = reactive([])
// 父传了多少个column插槽,就生成多少列
if (slots.column) {
// 注意:如果父传多个同名具名插槽,slots.column是数组?不,Vue3中同名具名插槽会被合并成一个数组传递,所以slots.column是一个函数,调用后返回多个VNode
const columnVNodes = slots.column()
columnVNodes.forEach((vnode, index) => {
columns.push({ key: index, content: vnode })
})
}
这样就能动态根据父组件传的插槽数量,生成表格列,让组件更灵活。
插槽逻辑与模板解耦,让代码更整洁
如果组件里插槽相关逻辑很复杂(比如要结合props、响应式数据判断插槽是否渲染),把这些逻辑放到setup
里用useSlots处理,能避免模板里写一堆v-if。
比如封装一个“可折叠的侧边栏”组件,侧边栏标题用#title
用默认插槽,需求是:“如果没传title插槽,就用props里的defaultTitle;折叠状态下隐藏内容插槽”,这时候用useSlots把判断逻辑集中到JS:
import { useSlots, computed } from 'vue'
export default {
props: { defaultTitle: String },
setup(props) {
const slots = useSlots()
// 处理标题:优先插槽,没有则用props
const titleContent = computed(() => {
return slots.title ? slots.title() : props.defaultTitle
})
// 处理内容:折叠时隐藏(假设hasCollapse是响应式数据)
const showContent = computed(() => !hasCollapse && slots.default)
return { titleContent, showContent }
}
}
模板里只需要绑定这些计算好的结果,逻辑和模板分离,维护起来更轻松。
useSlots和useAttrs经常一起出现,它们有啥区别?
不少同学学useSlots时,总会和useAttrs搞混,其实两者分工很明确:
- useSlots:专门处理(不管是默认插槽还是具名插槽),关注“父组件给子组件传了哪些可渲染的插槽片段”。
- useAttrs:专门处理属性和事件(那些没被props接收、也不是组件自定义事件的内容),比如父组件给子组件传了class、style、@click(但子组件没在emits里声明),这些都会被useAttrs捕获。
举个“按钮组件”的例子,看两者怎么配合:
父组件调用:
<MyButton
class="custom-btn"
@click="handleClick"
:disabled="false"
>
<template #icon><Icon name="arrow" /></template>
点击我
</MyButton>
子组件MyButton的setup:
import { useSlots, useAttrs } from 'vue'
export default {
props: ['disabled'],
emits: ['click'],
setup(props, { emit }) {
const slots = useSlots() // 拿到icon插槽和默认插槽
const attrs = useAttrs() // 拿到class、@click(因为@click没在emits声明,所以进attrs)
// 处理点击事件:从attrs里取@click,或者自己emit
const handleBtnClick = () => {
if (attrs.onClick) {
attrs.onClick() // 调用父传的@click
}
emit('click') // 触发声明的emits
}
return { slots, attrs, handleBtnClick }
模板中:
<button
:class="attrs.class"
:disabled="props.disabled"
@click="handleBtnClick"
>
<slot name="icon"></slot>
<slot></slot>
</button>
能看到:useSlots负责插槽内容(icon和默认文本),useAttrs负责属性(class)和未声明的事件(onClick),两者配合让组件封装更完整。
用useSlots做个实战案例:封装可定制的卡片组件
光说概念太抽象,咱们拿“卡片组件”做例子,看看useSlots怎么解决实际需求。
需求背景
要做一个Card组件,包含header、body、footer三个区域:
- 每个区域都支持父组件传插槽(header用#header,footer用#footer,body用默认插槽);
- 如果父没传插槽,显示默认内容;
- header和footer区域下方,根据“是否传了插槽”显示分隔线(传了插槽就隐藏分隔线,没传就显示)。
子组件Card的实现步骤
第一步:在setup里用useSlots拿到所有插槽,结合computed处理分隔线显示逻辑。
import { useSlots, computed } from 'vue'
export default {
setup() {
const slots = useSlots()
// 计算header分隔线:没传header插槽才显示
const showHeaderDivider = computed(() => !slots.header)
// 计算footer分隔线:没传footer插槽才显示
const showFooterDivider = computed(() => !slots.footer)
return { slots, showHeaderDivider, showFooterDivider }
}
}
第二步:写模板,把插槽、默认内容、分隔线逻辑结合起来。
<template>
<div class="card">
<div class="card-header">
<!-- 具名插槽header -->
<slot name="header">
<h2 class="default-header">默认卡片标题</h2>
</slot>
<!-- 分隔线:没传header插槽时显示 -->
<div class="divider" v-if="showHeaderDivider"></div>
</div>
<div class="card-body">
<!-- 默认插槽 -->
<slot>
<p class="default-body">这里是默认卡片内容~</p>
</slot>
</div>
<div class="card-footer">
<!-- 具名插槽footer -->
<slot name="footer">
<button class="default-btn">默认按钮</button>
</slot>
<!-- 分隔线:没传footer插槽时显示 -->
<div class="divider" v-if="showFooterDivider"></div>
</div>
</div>
</template>
第三步:父组件调用Card,测试不同插槽传递情况。
<template>
<div>
<!-- 传全部插槽 -->
<Card>
<template #header><h2>自定义标题</h2></template>
这是自定义内容~
<template #footer><button @click="handleClick">自定义按钮</button></template>
</Card>
<!-- 只传默认插槽 -->
<Card>只有默认内容</Card>
<!-- 啥插槽都不传 -->
<Card />
</div>
</template>
这个案例里,useSlots帮我们在JS层判断“父有没有传header/footer插槽”,再通过computed控制分隔线显示,既满足了“插槽自定义”的灵活性,又保证了“没传插槽时显示默认UI”的鲁棒性。
用useSlots时要避开哪些坑?
掌握用法后,这些细节不注意很容易踩雷,得重点关注:
插槽是函数,必须调用执行
很多同学会犯的错:把slots.default
直接放到模板里,比如写{{ slots.default }}
——这会把函数本身当字符串渲染,而不是渲染插槽内容。正确做法是调用函数:{{ slots.default() }}
(但注意,在模板中如果是Vue的模板语法,写<slot>默认内容</slot>更简洁,JS里处理才需要调用函数)。
响应式与更新时机
useSlots返回的slots对象本身不是响应式的,但的变化会触发组件重新渲染(因为父组件传插槽,父组件更新会带动子组件更新),所以不需要用reactive或ref包裹slots,直接用就行,比如父组件给子组件的插槽里加了个响应式数据,子组件里用slots拿到的内容会自动更新。
插槽名的大小写与格式
Vue3中,模板里的插槽名是kebab-case(my-slot),useSlots里拿到的键也是my-slot(保持一致),所以命名时要注意:别在模板用#mySlot,JS里找slots.my-slot——这样会找不到,统一用kebab-case更安全。
别过度依赖useSlots
如果只是“给插槽加默认内容”,用模板里的<slot>默认内容</slot>更简单,useSlots适合需要在JS逻辑中处理插槽的场景(比如结合计算属性、循环、复杂条件判断),盲目用useSlots会让代码变复杂,得根据需求选择。
把这些知识点串起来,useSlots其实就是Vue3给我们在组合式API里“灵活操作插槽”的钥匙,从判断插槽存在性,到动态处理多插槽逻辑,再到和useAttrs配合封装组件,它能解决很多以前靠$slots才能做的事,还让代码结构更清晰。
下次写组件时,碰到“插槽要不要显示”“插槽数量影响UI”这类需求,记得想想useSlots怎么用——把JS逻辑和插槽操作结合起来,组件封装会更顺手~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。