在 10.1 节介绍的默认插槽和具名插槽解决了“父组件往子组件里塞内容”的问题,但实际开发中还有一种很常见的需求:子组件内部有一些数据,希望交给父组件来决定如何渲染。这就是作用域插槽(Scoped Slots)的用武之地。
它解决什么问题
假设你在封装一个商品列表组件 <ProductList>,子组件内部已经拿到了商品数组,但每个卡片的 UI 样式在不同页面里可能完全不同——比如首页只显示名称和价格,搜索页要显示缩略图和评分,管理后台要显示编辑和删除按钮。你不可能为每一种样式都写一个专用组件,更合理的方式是:子组件提供数据,父组件决定展示方式。
作用域插槽的本质是“带有参数的插槽”。子组件在插槽上绑定一些数据,父组件通过一个模板变量接收这些数据,然后自由渲染。
基础语法
子组件(ProductList.vue):用 <slot :属性名="数据"> 的形式向外暴露数据:
<template>
<div class="product-list">
<div v-for="product in products" :key="product.id">
<!-- 通过插槽向外抛出 product 对象 -->
<slot name="card" :product="product" />
</div>
</div>
</template>
<script setup>
defineProps(['products'])
</script>
父组件:用 v-slot:插槽名="变量名" 接收数据,然后在模板中自由使用:
<template>
<ProductList :products="productList">
<!-- 接收子组件抛出的 product 数据 -->
<template v-slot:card="{ product }">
<div class="card">
<h3>{{ product.name }}</h3>
<p>¥{{ product.price }}</p>
</div>
</template>
</ProductList>
</template>
如果只有一个默认插槽且需要传递数据,可以直接用 v-slot="slotProps":
<!-- 子组件 -->
<slot :user="currentUser" />
<!-- 父组件 -->
<MyComponent v-slot="{ user }">
<span>{{ user.name }}</span>
</MyComponent>
实战示例:表格列的自定义渲染
作用域插槽最常见的场景之一就是表格组件封装。表格组件负责分页、排序、数据循环,但每列的渲染方式交给使用者决定:
子组件(DataTable.vue):
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">{{ col.title }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="col in columns" :key="col.key">
<!-- 动态插槽名,将整行数据 row 抛出去 -->
<slot :name="col.key" :row="row">
<!-- 默认直接显示字段值 -->
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>
<script setup>
defineProps(['columns', 'data'])
</script>
父组件:
<template>
<DataTable :columns="columns" :data="users">
<!-- 状态列:自定义成带颜色的圆点 -->
<template v-slot:status="{ row }">
<span :style="{ color: row.status === 'active' ? 'green' : 'gray' }">
● {{ row.status }}
</span>
</template>
<!-- 操作列:自定义成按钮 -->
<template v-slot:action="{ row }">
<button @click="handleEdit(row)">编辑</button>
<button @click="handleDelete(row.id)">删除</button>
</template>
</DataTable>
</template>
<script setup>
const columns = [
{ key: 'name', title: '姓名' },
{ key: 'email', title: '邮箱' },
{ key: 'status', title: '状态' },
{ key: 'action', title: '操作' }
]
const users = [
{ id: 1, name: 'Alice', email: 'a@example.com', status: 'active' },
{ id: 2, name: 'Bob', email: 'b@example.com', status: 'inactive' }
]
</script>
这个例子中,表格组件本身不关心“状态怎么展示”或“操作按钮长什么样”,它只负责遍历数据并抛出每一行的数据。父组件拿到 row 对象后,可以用任何喜欢的方式渲染那一列。这就是作用域插槽的核心价值:让复用组件真正做到了“样式与数据分离”,数据归子组件管,渲染权归父组件。
与普通插槽的本质区别
- 普通插槽:
<slot></slot>接收的是父组件写好的静态或动态内容,子组件无法向其中注入数据。 - 作用域插槽:
<slot :data="something"></slot>相当于子组件声明了一个“回调接口”,告诉父组件:“我这里有一些数据,你想怎么用就怎么用,我给你传过去。”
这使得组件封装边界更清晰:子组件负责获取/处理数据(如 API 请求、格式化),父组件负责 UI 呈现。当需要更换 UI 时,只需修改父组件的插槽模板,子组件逻辑完全不动。
一个小提醒
- 解构语法
v-slot="{ user }"可以让你直接拿到需要的数据,不用每次slotProps.user。 - Vue 3 中,
v-slot只能用在<template>标签或组件标签上(当只使用默认插槽时可以直接写在组件标签上)。 - 作用域插槽的数据是响应式的,子组件的数据变了,父组件会同步更新。