讲解
props 传数据,插槽传「内容」。一个卡片组件,标题、正文、底部按钮各不相同,不该做成十几个 props,而该留几个「空洞」让使用方填——这就是插槽。默认插槽对应组件标签里的内容:<Card>这里是内容</Card>,Card 内部用 <slot></slot> 承接。具名插槽解决「多个洞」:<slot name="header"> 配合 <template #header>,一个组件可以有头部、正文、底部多个分发口。
插槽内容的一个关键规则是作用域:插槽内容在父组件作用域编译,访问的是父组件的数据——<Card>{{ userName }}</Card> 里的 userName 是父组件的,跟 Card 内部的状态无关。这符合直觉:内容是父组件写的。
但反过来怎么办?子组件循环渲染列表,每一项的展示想交给父组件定制,父组件又需要拿到「当前这一项」的数据——这就是作用域插槽:子组件在 <slot :item="row"> 上把数据抛出来,父组件用 <template #default="{ item }"> 解构接收。数据从子流向父,渲染权从父回到父,表格、列表、下拉选项组件全靠这个模式撑起来。配合 v-if 还可以给插槽写兜底内容:<slot>默认文案</slot>,使用方不传时显示。
示例
用「函数传参」类比作用域插槽:插槽本质上就是子组件回调父组件传下来的渲染函数(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 子组件:渲染列表,每一项的 HTML 由「插槽函数」决定
function List({ items }, slots) {
return items
.map((item) => {
const render = slots.item || (({ item }) => item.text); // 兜底内容
return '<li>' + render({ item, index: items.indexOf(item) }) + '</li>';
})
.join('');
}
// 场景一:父组件不提供插槽 → 用兜底
const plain = List({ items: [{ text: '苹果' }, { text: '香蕉' }] }, {});
assert.strictEqual(plain, '<li>苹果</li><li>香蕉</li>');
// 场景二:父组件提供作用域插槽 → 完全自定义
const fancy = List(
{ items: [{ text: '苹果' }, { text: '香蕉' }] },
{
item: ({ item, index }) => '<b>' + (index + 1) + '. ' + item.text + '</b>',
},
);
assert.strictEqual(fancy, '<li><b>1. 苹果</b></li><li><b>2. 香蕉</b></li>');
console.log('兜底渲染:', plain);
console.log('作用域插槽:', fancy);
console.log('数据在子组件,渲染权在父组件——这就是作用域插槽');
真实组件:
<script setup>
// DataList.vue
defineProps({ items: { type: Array, required: true } });
</script>
<template>
<ul>
<li v-for="(item, index) in items" :key="index">
<slot name="item" :item="item" :index="index">
{{ item.text }}
</slot>
</li>
</ul>
</template>
<script setup>
// Parent.vue
import { ref } from 'vue';
import DataList from './DataList.vue';
const fruits = ref([{ text: '苹果' }, { text: '香蕉' }]);
</script>
<template>
<DataList :items="fruits">
<template #item="{ item, index }">
<b>{{ index + 1 }}. {{ item.text }}</b>
</template>
</DataList>
</template>
常见坑
- 搞混插槽的作用域:插槽里直接访问子组件内部变量是拿不到的,要子组件通过 slot props 显式抛出来。
- 具名插槽名字不匹配:
<slot name="header">对应<template #header>(或 v-slot:header),名字差一个字符就静默走兜底。 - 在循环外解构 slot props:作用域插槽的数据每一项都不同,template 必须放在 v-for 对应的层级。
- 把复杂逻辑塞进插槽内容:插槽内容也是模板,复杂推导先 computed 再填进来。
- 过度设计插槽:只有一两个定制点时 props 更简单,插槽留给「整块内容都要定制」的场景。
小结
插槽分发内容、作用域归父组件;作用域插槽让子组件把数据抛给父组件渲染——列表/表格组件的核心模式。下一章看组件从出生到销毁的全过程:生命周期。