在开发组件时,props 只能传递数据,但很多场景下我们需要传递模板内容。
比如一个按钮组件,我们希望它有统一的样式,但内部文字或图标由外部决定。这时就用到插槽(slot)。
你可以把插槽理解为:组件的“占位符”,由父组件决定填充的内容。
父组件使用:
<!-- 父组件 -->
<FancyButton>
点我一下! <!-- 插槽内容 -->
</FancyButton>
子组件 FancyButton.vue:
<template>
<button class="fancy-btn">
<!-- 插槽出口,父组件传什么,这里就显示什么 -->
<slot></slot>
</button>
</template>
渲染结果:
<button class="fancy-btn">点我一下!</button>
这样 FancyButton 的样式固定,但内容灵活。
如果父组件没有传递内容,可以给插槽设置默认值。
子组件 SubmitButton.vue:
<template>
<button type="submit">
<slot>提交</slot>
<!-- 默认是“提交” -->
</button>
</template>
父组件:
<SubmitButton />
<!-- 没传内容 -->
<SubmitButton>保存</SubmitButton>
<!-- 传了内容 -->
结果:
<button type="submit">提交</button> <button type="submit">保存</button>
当组件内部有多个位置需要插入不同内容时:
BaseLayout.vue:
<template>
<div class="container">
<header><slot name="header"></slot></header>
<main><slot></slot></main>
<!-- 默认插槽 -->
<footer><slot name="footer"></slot></footer>
</div>
</template>
父组件使用:
<BaseLayout>
<template #header>
<h1>我是标题</h1>
</template>
<p>正文内容...</p> <!-- 默认插槽 -->
<template #footer>
<p>我是底部信息</p>
</template>
</BaseLayout>
渲染结果示意:
+--------------------+
| 我是标题 |
+--------------------+
| 正文内容... |
+--------------------+
| 我是底部信息 |
+--------------------+
具名插槽就是给“占位符”贴标签,方便对应填充。
如果某个插槽内容没有传递,可以通过 $slots 判断是否需要渲染:
父组件传 header:
<Card>
<template #header>头部</template>
</Card>
Card.vue:
<template>
<div class="card">
<div v-if="$slots.header"><slot name="header" /></div>
<div v-if="$slots.default"><slot /></div>
<div v-if="$slots.footer"><slot name="footer" /></div>
</div>
</template>
插槽名可动态绑定:
<base-layout>
<template v-slot:[dynamicSlot]>
动态内容
</template>
</base-layout>
默认情况下,插槽内容只能访问父组件的数据。但有时我们希望子组件把一些数据“传出来”,让父组件在插槽里使用。
子组件示例:
<!-- MyComponent.vue -->
<template>
<slot :text="greeting" :count="1"></slot>
</template>
<script setup>
const greeting = '你好'
</script>
父组件使用:
<MyComponent v-slot="{ text, count }">
{{ text }} - 次数:{{ count }}
</MyComponent>
渲染结果:
你好 - 次数:1
类比 JavaScript:
function MyComponent(slotFn) {
const data = { text: '你好', count: 1 }
return slotFn(data) // 调用函数并传入参数
}
所以作用域插槽可以理解为:子组件给父组件“回调函数”传参。
<MyComponent>
<template #header="{ message }">
<h2>{{ message }}</h2>
</template>
</MyComponent>
子组件:
<slot name="header" :message="'来自子组件的数据'"></slot>
FancyList.vue:
<template>
<ul>
<li v-for="item in items" :key="item.id">
<!-- 把 item 数据传给插槽 -->
<slot name="item" v-bind="item"></slot>
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, text: 'Vue', likes: 100 },
{ id: 2, text: 'React', likes: 200 }
])
</script>
父组件:
<FancyList>
<template #item="{ text, likes }">
<p>{{ text }} ❤️ {{ likes }}</p>
</template>
</FancyList>
渲染结果:
Vue ❤️ 100
React ❤️ 200
封装逻辑但不渲染视图,内容交给父组件:
MouseTracker.vue:
<template>
<slot :x="x" :y="y"></slot>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const x = ref(0),
y = ref(0)
onMounted(() => {
window.addEventListener('mousemove', e => {
x.value = e.pageX
y.value = e.pageY
})
})
</script>
父组件:
<MouseTracker v-slot="{ x, y }">
鼠标位置:{{ x }} , {{ y }}
</MouseTracker>
父组件 ----> 子组件
│ │
│ props │
│ slot内容 │ <slot> 占位符
▼ ▼
插槽渲染 = 父组件内容 + 子组件外壳
$slots 判断是否存在内容。插槽的核心价值:让组件更灵活、可复用,同时保留样式与逻辑的封装性。