一、插槽是什么
插槽是 Vue.js 中用于组件内容分发的机制,它允许你在组件内部预留位置,让使用该组件的人可以传入自定义内容。这就好比在组件中预留了一个「坑」,使用组件的人可以往这个坑里填入任何内容。
插槽的核心价值在于实现组件的复用性和灵活性。通过插槽,我们可以将组件的外壳与内容分离,使得同一个组件可以适应不同的使用场景。
二、插槽的基本用法
2.1 简单插槽
<!-- ChildComponent.vue -->
<template>
<div class="card">
<h2>卡片标题</h2>
<slot></slot>
</div>
</template>
<!-- ParentComponent.vue -->
<template>
<ChildComponent>
<p>这是通过插槽传入的内容</p>
</ChildComponent>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue';
</script>
渲染结果:
<div class="card">
<h2>卡片标题</h2>
<p>这是通过插槽传入的内容</p>
</div>
2.2 插槽的默认内容
当使用组件时没有传入任何内容,插槽可以显示默认内容:
<!-- Button.vue -->
<template>
<button class="btn">
<slot>默认按钮文字</slot>
</button>
</template>
<!-- 使用 -->
<Button>点击我</Button> <!-- 显示「点击我」 -->
<Button /> <!-- 显示「默认按钮文字」 -->
2.3 具名插槽
当一个组件需要多个插槽位置时,可以使用具名插槽:
<!-- BaseLayout.vue -->
<template>
<div class="layout">
<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>
2.4 作用域插槽
作用域插槽允许插槽内容访问子组件的数据:
<!-- UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id">
<slot :user="user" :index="$index"></slot>
</li>
</ul>
</template>
<script setup>
const users = ref([
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' }
]);
</script>
<!-- 使用作用域插槽 -->
<UserList v-slot="{ user, index }">
<span>{{ index + 1 }}. {{ user.name }} - {{ user.email }}</span>
</UserList>
三、插槽的高级用法
3.1 动态插槽名
Vue 3 支持动态插槽名:
<template>
<div>
<slot :name="dynamicSlotName"></slot>
</div>
</template>
<script setup>
const dynamicSlotName = ref('header');
</script>
<MyComponent>
<template #[dynamicSlotName]>
<p>动态插槽内容</p>
</template>
</MyComponent>
3.2 插槽的解构
作用域插槽可以解构:
<!-- 使用解构 -->
<UserList v-slot="{ user: person, index: i }">
<span>{{ i + 1 }}. {{ person.name }}</span>
</UserList>
3.3 默认插槽简写
当只有默认插槽时,可以不使用 template:
<!-- 完整写法 -->
<BaseLayout>
<template #default>
<p>内容</p>
</template>
</BaseLayout>
<!-- 简写(仅默认插槽) -->
<BaseLayout>
<p>内容</p>
</BaseLayout>
3.4 具名插槽的缩写
Vue 3 支持 v-slot 的缩写 #:
<!-- 完整写法 -->
<BaseLayout>
<template v-slot:header>
<h1>标题</h1>
</template>
</BaseLayout>
<!-- 缩写 -->
<BaseLayout>
<template #header>
<h1>标题</h1>
</template>
</BaseLayout>
四、插槽的实际应用场景
4.1 卡片组件
<!-- Card.vue -->
<template>
<div class="card" :class="{ 'card--hoverable': hoverable }">
<div v-if="$slots.header || title" class="card__header">
<slot name="header">
<h3 class="card__title">{{ title }}</h3>
</slot>
</div>
<div class="card__body">
<slot></slot>
</div>
<div v-if="$slots.footer" class="card__footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script setup>
defineProps({
title: String,
hoverable: {
type: Boolean,
default: false
}
});
</script>
<!-- 使用 -->
<Card title="用户信息" :hoverable="true">
<template #header>
<div class="flex justify-between">
<h3>用户详情</h3>
<span class="badge">VIP</span>
</div>
</template>
<p>姓名:张三</p>
<p>邮箱:zhangsan@example.com</p>
<template #footer>
<button class="btn btn-primary">编辑</button>
</template>
</Card>
4.2 列表组件
<!-- DataList.vue -->
<template>
<div class="data-list">
<div v-if="$slots.header" class="data-list__header">
<slot name="header"></slot>
</div>
<div class="data-list__content">
<div
v-for="(item, index) in items"
:key="getKey(item, index)"
class="data-list__item"
>
<slot :item="item" :index="index" name="item"></slot>
</div>
</div>
<div v-if="$slots.empty && items.length === 0" class="data-list__empty">
<slot name="empty"></slot>
</div>
<div v-if="$slots.footer" class="data-list__footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script setup>
const props = defineProps({
items: {
type: Array,
default: () => []
},
getKey: {
type: Function,
default: (item, index) => item.id ?? index
}
});
</script>
<!-- 使用 -->
<DataList :items="users">
<template #header>
<h3>用户列表</h3>
</template>
<template #item="{ item, index }">
<div class="user-item">
<span class="index">{{ index + 1 }}</span>
<span class="name">{{ item.name }}</span>
<span class="email">{{ item.email }}</span>
</div>
</template>
<template #empty>
<p>暂无用户数据</p>
</template>
<template #footer>
<Pagination :total="total" />
</template>
</DataList>
4.3 弹窗组件
<!-- Modal.vue -->
<template>
<Teleport to="body">
<Transition name="modal">
<div v-if="modelValue" class="modal-mask" @click.self="close">
<div class="modal-container" :style="{ width: width }">
<div class="modal__header">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
<button v-if="closable" class="modal__close" @click="close">×</button>
</div>
<div class="modal__body">
<slot></slot>
</div>
<div v-if="$slots.footer" class="modal__footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
const props = defineProps({
modelValue: Boolean,
title: String,
width: {
type: String,
default: '500px'
},
closable: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['update:modelValue']);
function close() {
if (props.closable) {
emit('update:modelValue', false);
}
}
</script>
<!-- 使用 -->
<Modal v-model="showModal" title="编辑用户" width="600px">
<form @submit.prevent="saveUser">
<div class="form-group">
<label>姓名</label>
<input v-model="form.name" />
</div>
<div class="form-group">
<label>邮箱</label>
<input v-model="form.email" />
</div>
</form>
<template #footer>
<button @click="showModal = false">取消</button>
<button class="primary" @click="saveUser">保存</button>
</template>
</Modal>
4.4 表单组件
<!-- FormBuilder.vue -->
<template>
<form class="form-builder" @submit.prevent="handleSubmit">
<div
v-for="field in fields"
:key="field.name"
class="form-field"
>
<label :for="field.name">{{ field.label }}</label>
<slot
:name="'field-' + field.type"
:field="field"
:value="formData[field.name]"
:update="(val) => formData[field.name] = val"
>
<input
:id="field.name"
:type="field.type"
:value="formData[field.name]"
@input="formData[field.name] = $event.target.value"
/>
</slot>
<span v-if="errors[field.name]" class="error">{{ errors[field.name] }}</span>
</div>
<div class="form-actions">
<slot name="actions" :submit="handleSubmit">
<button type="submit">提交</button>
</slot>
</div>
</form>
</template>
<script setup>
const props = defineProps({
fields: {
type: Array,
required: true
},
initialData: {
type: Object,
default: () => ({})
}
});
const formData = reactive({ ...props.initialData });
const errors = reactive({});
function handleSubmit() {
// 验证和提交逻辑
}
</script>
<!-- 使用 -->
<FormBuilder :fields="formFields" :initial-data="initialData">
<template #field-textarea="{ field, value, update }">
<textarea
:id="field.name"
:value="value"
@input="update($event.target.value)"
rows="4"
></textarea>
</template>
<template #field-select="{ field, value, update }">
<select :id="field.name" :value="value" @change="update($event.target.value)">
<option v-for="opt in field.options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</template>
<template #actions="{ submit }">
<button @click="submit">确认提交</button>
<button type="button" @click="resetForm">重置</button>
</template>
</FormBuilder>
五、插槽与组合式 API
5.1 useSlots 钩子
Vue 3 提供了 useSlots 来访问插槽:
<script setup>
import { useSlots, useAttrs } from 'vue';
const slots = useSlots();
const attrs = useAttrs();
// 检查插槽是否存在
console.log(slots.default); // 默认插槽
console.log(slots.header); // 具名插槽 header
console.log(slots.footer); // 具名插槽 footer
// 动态使用插槽
if (slots.custom) {
console.log('custom 插槽存在');
}
</script>
5.2 渲染函数中使用插槽
// 使用渲染函数
h('div', [
slots.header({ level: 1 }, 'Header content'),
slots.default({ items: props.items }),
slots.footer()
]);
5.3 高级:自定义渲染器
<script setup>
import { h } from 'vue';
const MyComponent = {
setup(props, { slots }) {
return () => {
return h('div', { class: 'container' }, [
slots.header?.() || h('h2', 'Default Header'),
slots.default?.() || h('p', 'Default Content'),
slots.footer?.() || null
]);
};
}
};
</script>
六、插槽的性能优化
6.1 避免不必要的重渲染
<!-- ❌ 错误:每次渲染都创建新函数 -->
<template>
<ChildComponent>
<template #default>
<ExpensiveComponent :data="data" @update="(val) => handleUpdate(val)" />
</template>
</ChildComponent>
</template>
<script setup>
function handleUpdate(val) {
// 处理更新
}
</script>
<!-- ✅ 正确:使用稳定的函数引用 -->
<template>
<ChildComponent>
<template #default>
<ExpensiveComponent :data="data" @update="handleUpdate" />
</template>
</ChildComponent>
</template>
<script setup>
const handleUpdate = (val) => {
// 处理更新
};
</script>
6.2 使用 v-memo 优化
<template>
<div>
<slot :items="items" :memo="memoValue"></slot>
</div>
</template>
<script setup>
const items = ref([]);
const memoValue = computed(() => {
return items.value.map(item => item.id);
});
</script>
七、常见问题与解决方案
7.1 插槽内容不显示
<!-- ❌ 错误:v-if 阻止了插槽渲染 -->
<template>
<div>
<slot v-if="false"></slot>
</div>
</template>
<!-- ✅ 正确:使用 v-show -->
<template>
<div>
<slot v-show="true"></slot>
</div>
</template>
7.2 作用域插槽的数据响应式
<!-- 确保传递的是响应式数据 -->
<template>
<div>
<slot :items="items"></slot>
</div>
</template>
<script setup>
const items = ref([]);
// ❌ 错误:直接修改不会触发更新
items.value = newItems;
// ✅ 正确:使用响应式方式
items.value.splice(0, items.value.length, ...newItems);
</script>
7.3 动态组件的插槽
<template>
<component :is="currentComponent">
<template v-for="(_, name) in $slots" #[name]>
<slot :name="name"></slot>
</template>
</component>
</template>
八、最佳实践总结
8.1 插槽命名规范
<!-- 推荐:使用清晰的命名 -->
<slot name="header"></slot>
<slot name="body"></slot>
<slot name="footer"></slot>
<slot name="actions"></slot>
<!-- 推荐:使用前缀区分 -->
<slot name="field-text"></slot>
<slot name="field-select"></slot>
<slot name="field-checkbox"></slot>
8.2 插槽文档化
<!-- Modal.vue -->
/**
* @slot header - 弹窗头部内容
* @slot default - 弹窗主体内容
* @slot footer - 弹窗底部内容(通常放置操作按钮)
*/
8.3 插槽的版本兼容
// Vue 2 写法
<template>
<slot></slot>
</template>
// Vue 3 写法(推荐)
<template>
<slot></slot>
</template>
<script setup>
// Vue 3 组合式 API
</script>
九、总结
插槽是 Vue 组件化开发的核心概念之一,它提供了灵活的内容分发机制:
- 基本插槽:用于简单的内容分发
- 具名插槽:用于组件的多个位置
- 作用域插槽:用于传递子组件数据给插槽内容
- 动态插槽:用于运行时决定使用哪个插槽
掌握插槽的使用,可以让你构建出更加灵活、可复用的 Vue 组件。记住插槽的最佳实践,合理命名、文档化、避免不必要的重渲染,你的组件将会更加专业和高效。
如果这篇文章对你有帮助,欢迎点赞收藏。