Code前端首页关于Code前端联系我们

useSlots到底是什么?和选项式的$slots有啥区别?

terry 15小时前 阅读数 11 #Vue
文章标签 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的基础用法是怎样的?得掌握哪些关键点?

先看最基础的语法流程:

  1. 导入API:在组件的setup函数里,先导入useSlots → import { useSlots } from 'vue';
  2. 调用获取插槽对象:const slots = useSlots(); 这里的slots是个对象,存储着所有传入的插槽。
  3. 访问具体插槽:比如判断默认插槽是否存在 → slots.default;判断具名插槽header是否存在 → slots.header
  4. 渲染插槽:因为插槽本质是渲染函数,所以要用函数调用的方式渲染,比如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前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门