作用域插槽:把数据交出去
插槽与组件组合 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
作用域插槽解决「子组件有数据、父组件有渲染需求」的矛盾。这一节掌握 v-slot 接收插槽 props、解构写法、以及它和 React render props 的对应关系。学完你能设计出「子组件管逻辑、父组件管呈现」的组件 API。
问题场景
一个列表组件封装了加载、空状态、错误状态,但每行怎么渲染由调用方决定——有的页面显示卡片,有的显示表格行。用 props 传「渲染模式」字符串会让组件内部堆满 v-if;把数据全交给父组件又会重复实现加载逻辑。作用域插槽让子组件把「数据」和「渲染时机」交出去,父组件只写「长什么样」。
核心解释
子组件在 <slot> 上绑定数据,这些绑定会作为「插槽 props」传给父组件的插槽内容:
<!-- components/DataList.vue -->
<script setup>
defineProps({
items: { type: Array, required: true },
loading: { type: Boolean, default: false },
});
</script>
<template>
<div class="data-list">
<p v-if="loading">加载中…</p>
<p v-else-if="items.length === 0">暂无数据</p>
<ul v-else>
<li v-for="(item, index) in items" :key="item.id">
<!-- 把 item 和 index 交给父组件 -->
<slot :item="item" :index="index" />
</li>
</ul>
</div>
</template>
父组件用 #default="{ item, index }" 接收并解构:
<script setup>
import DataList from "./components/DataList.vue";
const courses = [
{ id: 1, title: "Vue 3 全景", hours: 1 },
{ id: 2, title: "模板语法", hours: 1.25 },
];
</script>
<template>
<DataList :items="courses">
<template #default="{ item, index }">
<strong>{{ index + 1 }}. {{ item.title }}</strong>
<span>({{ item.hours }}h)</span>
</template>
</DataList>
</template>
同一个 DataList,换个页面就能渲染成表格:
换一个页面只需替换插槽内容,比如渲染成表格行:<template #default="{ item }"><tr><td>{{ item.title }}</td><td>{{ item.hours }}h</td></tr></template>。
与 React 的对应:作用域插槽 ≈ render props(children 传函数)。React 里写 <List>{(item) => <Row item={item} />}</List>,Vue 里写 <template #default="{ item }">。心智模型一致:逻辑在子组件,渲染在父组件。
解构的边界:#default="{ item }" 解构的是插槽 props 对象,解构后 item 是普通值——如果子组件替换了 item 引用(而不是改属性),父组件模板会拿到新值(插槽每次渲染重新调用),所以解构是安全的。这点和 props 解构不同,不用担心响应式丢失。
多个作用域插槽:一个组件可以同时暴露多个带数据的插槽,比如表格组件的 #cell(每个单元格)和 #empty(空状态)。命名要按「数据语义」而不是「位置」。
示例拆解
一个「异步边界」组件:统一处理 loading/error/data 三态,把成功数据交给父组件渲染:
<!-- components/AsyncBoundary.vue -->
<script setup>
defineProps({
loading: Boolean,
error: { type: String, default: "" },
});
const emit = defineEmits(["retry"]);
</script>
<template>
<div>
<p v-if="loading" class="state">加载中…</p>
<div v-else-if="error" class="state error">
{{ error }}
<button @click="emit('retry')">重试</button>
</div>
<slot v-else name="content" />
</div>
</template>
<!-- 使用 -->
<script setup>
import { onMounted, ref } from "vue";
import AsyncBoundary from "./components/AsyncBoundary.vue";
const loading = ref(true);
const error = ref("");
const users = ref([]);
async function load() {
loading.value = true;
error.value = "";
try {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
users.value = await res.json();
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
onMounted(load);
</script>
<template>
<AsyncBoundary :loading="loading" :error="error" @retry="load">
<template #content>
<ul>
<li v-for="u in users" :key="u.id">{{ u.name }}</li>
</ul>
</template>
</AsyncBoundary>
</template>
这个组件没有作用域插槽(数据仍在父组件),但展示了插槽的另一种职责:控制渲染时机。作用域插槽适合「子组件持有数据」,普通插槽适合「父组件持有数据、子组件决定何时显示」。
动手任务
- 完成
DataList,用同一份数据渲染出「卡片列表」和「表格」两种形态。 - 给
DataList加一个#empty具名插槽,允许父组件自定义空状态文案和图标。 - 写一个
Toggle组件:自己管理开关状态,通过作用域插槽把{ on, toggle }交给父组件,父组件用它渲染任意样式的开关。
常见错误
- 在子组件里用
item渲染插槽内容:作用域插槽的数据是给父组件模板用的,子组件只负责绑定。 - 解构时名字写错:
<slot :item="item" />对应#default="{ item }",写成{ data }拿到 undefined。 - 忘记默认插槽的
#default:作用域插槽必须写成<template #default="slotProps">,直接写在组件标签上不支持解构语法。 - 把作用域插槽当事件用:插槽是渲染通道,不能靠它回传用户操作;操作仍然走 emits。
检查与复盘
- 能写出「子组件绑定数据、父组件解构接收」的完整作用域插槽
- 能说出作用域插槽与 React render props 的对应关系
- 能判断一个组件该用普通插槽还是作用域插槽
参考资料
- Vue 官方文档「插槽」:作用域插槽与解构
- Vue 官方文档「渲染函数与 JSX」:与 render props 的对照
读完这一课了?
标记完成后,工作台会安排接下来的学习。