Vue 3 插槽系统详解:具名插槽、作用域插槽与动态插槽

组件化开发中,插槽(Slots)是 Vue 实现内容分发最核心的机制之一。它让父组件能够向子组件传递模板片段,从而在保持组件封装性的同时获得极高的灵活性。Vue 3 在延续 Vue 2 插槽理念的基础上,对语法进行了统一和简化,同时引入了动态插槽名等新能力。本文将系统梳理 Vue 3 中具名插槽、作用域插槽与动态插槽的用法与最佳实践。

为什么需要插槽

假设你封装了一个 <Card> 组件,它负责渲染卡片的外框、阴影和圆角。但卡片内部要放什么内容,组件本身无法预知。如果没有插槽,你只能通过 props 传递字符串或 HTML,既笨拙又不安全。插槽的出现,让父组件可以像写原生 HTML 一样,把任意模板结构“塞”进子组件的指定位置。

<!-- Card.vue -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header" />
    </div>
    <div class="card-body">
      <slot />
    </div>
  </div>
</template>

父组件使用时:

<Card>
  <template #header>
    <h3>用户信息</h3>
  </template>
  <p>这里是卡片正文内容。</p>
</Card>

<slot> 标签就是内容分发的出口,父组件传入的模板会替换掉对应的 <slot>

具名插槽:精准投放内容

当一个组件需要多个分发出口时,就需要用到具名插槽。子组件通过 <slot name="xxx"> 声明具名出口,父组件通过 <template #xxx><template v-slot:xxx> 向指定出口填充内容。

<!-- Layout.vue -->
<template>
  <div class="layout">
    <header>
      <slot name="header">默认头部</slot>
    </header>
    <main>
      <slot>默认内容</slot>
    </main>
    <footer>
      <slot name="footer" />
    </footer>
  </div>
</template>

父组件填充:

<Layout>
  <template #header>
    <nav>导航栏</nav>
  </template>

  <!-- 未指定 name 的内容进入默认插槽 -->
  <section>主内容区</section>

  <template #footer>
    <p>版权信息</p>
  </template>
</Layout>

几个关键点值得注意:

  • 默认插槽没有显式 name,等价于 name="default"
  • <slot> 标签内部的内容是后备内容,当父组件未提供对应插槽内容时才会渲染。
  • #headerv-slot:header 的缩写,二者完全等价。

作用域插槽:让父组件访问子组件数据

普通插槽的渲染作用域在父组件,子组件的数据对插槽内容不可见。但有些场景下,子组件希望把自身的数据“暴露”给父组件,让父组件决定如何渲染。这就是作用域插槽的用武之地。

子组件在 <slot> 上绑定属性,这些属性会作为插槽 props 传递给父组件:

<!-- UserList.vue -->
<script setup>
const users = [
  { id: 1, name: '张三', age: 28 },
  { id: 2, name: '李四', age: 32 }
]
</script>

<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      <slot name="item" :user="user" :index="user.id">
        {{ user.name }}
      </slot>
    </li>
  </ul>
</template>

父组件通过 v-slot 的指令值接收这些数据:

<UserList>
  <template #item="{ user, index }">
    <span>{{ index }}. {{ user.name }}({{ user.age }} 岁)</span>
  </template>
</UserList>

这里使用了 ES2015 的解构语法,{ user, index } 对应子组件绑定的 :user:index。这种模式在封装表格、列表、下拉选择等组件时极为常见,因为它把“数据逻辑”留在子组件,把“渲染逻辑”交给父组件。

默认插槽的作用域写法

如果只有一个默认插槽且需要传递作用域数据,可以简写为:

<!-- 子组件 -->
<slot :item="item" />

<!-- 父组件 -->
<MyComp v-slot="{ item }">
  {{ item.title }}
</MyComp>

注意:当同时使用默认插槽和作用域时,v-slot 直接写在组件标签上,而不是 <template> 上。

动态插槽名

Vue 3 允许插槽名是动态的,通过 v-slot:[dynamicSlotName] 或简写 #[dynamicSlotName] 实现:

<template>
  <div>
    <button v-for="tab in tabs" :key="tab" @click="currentTab = tab">
      {{ tab }}
    </button>

    <BaseLayout>
      <template #[currentTab]>
        <component :is="currentTab + 'Panel'" />
      </template>
    </BaseLayout>
  </div>
</template>

动态插槽名在实现标签页切换、动态布局等场景时非常有用。需要留意的是,动态插槽名的表达式需要遵循 JavaScript 标识符的命名规则,且不能包含空格或引号。

插槽的编译与性能要点

理解插槽的底层机制有助于写出更高效的代码。在 Vue 3 中:

  1. 插槽内容在父组件编译,以函数形式传递给子组件。子组件渲染时调用这些函数,因此插槽内容的响应式依赖归属于父组件。
  2. 普通插槽与作用域插槽在编译后统一为函数,Vue 3 不再区分二者的内部实现,简化了运行时逻辑。
  3. 如果插槽内容依赖子组件数据,每次子组件更新都会重新调用插槽函数。对于大型列表,应尽量把循环放在子组件内部,而不是在父组件插槽中循环,以避免不必要的重渲染。

实践建议

  • 命名清晰:具名插槽的 name 使用 kebab-case,与组件 props 命名风格保持一致。
  • 提供后备内容:在 <slot> 内写默认内容,让组件在未传插槽时也能正常显示。
  • 作用域数据用对象传递:当需要暴露多个数据时,统一用对象解构,便于后续扩展。
  • 避免过度使用动态插槽名:动态插槽名会降低模板可读性,仅在真正需要动态切换时使用。

小结

Vue 3 的插槽系统围绕 <slot> 出口和 v-slot 指令展开,具名插槽解决了多出口分发问题,作用域插槽打通了子组件到父组件的数据通道,动态插槽名则提供了运行时的灵活性。三者结合,构成了 Vue 组件内容分发的完整能力。掌握它们,不仅能写出更优雅的组件 API,也能在封装复杂业务组件时游刃有余。

未经允许不得转载:任鹏个人博客 » Vue 3 插槽系统详解:具名插槽、作用域插槽与动态插槽

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏