组件化开发中,插槽(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>标签内部的内容是后备内容,当父组件未提供对应插槽内容时才会渲染。#header是v-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 中:
- 插槽内容在父组件编译,以函数形式传递给子组件。子组件渲染时调用这些函数,因此插槽内容的响应式依赖归属于父组件。
- 普通插槽与作用域插槽在编译后统一为函数,Vue 3 不再区分二者的内部实现,简化了运行时逻辑。
- 如果插槽内容依赖子组件数据,每次子组件更新都会重新调用插槽函数。对于大型列表,应尽量把循环放在子组件内部,而不是在父组件插槽中循环,以避免不必要的重渲染。
实践建议
- 命名清晰:具名插槽的 name 使用 kebab-case,与组件 props 命名风格保持一致。
- 提供后备内容:在
<slot>内写默认内容,让组件在未传插槽时也能正常显示。 - 作用域数据用对象传递:当需要暴露多个数据时,统一用对象解构,便于后续扩展。
- 避免过度使用动态插槽名:动态插槽名会降低模板可读性,仅在真正需要动态切换时使用。
小结
Vue 3 的插槽系统围绕 <slot> 出口和 v-slot 指令展开,具名插槽解决了多出口分发问题,作用域插槽打通了子组件到父组件的数据通道,动态插槽名则提供了运行时的灵活性。三者结合,构成了 Vue 组件内容分发的完整能力。掌握它们,不仅能写出更优雅的组件 API,也能在封装复杂业务组件时游刃有余。
未经允许不得转载:任鹏个人博客 » Vue 3 插槽系统详解:具名插槽、作用域插槽与动态插槽


朋友圈点赞图在线生成源码