很多人学 Vue 的时候,slot 这一关过得不太踏实。语法看懂了,但心里总有个疑问:我直接多传几个 props 不也能实现吗,为什么要用 slot?
这篇文章不讲具体语法,只讲设计理念。把这个想清楚了,slot 自然就会用了。
一、先从 Props 的边界说起
Props 是 Vue 组件通信最基础的方式,父组件把数据传给子组件,子组件负责渲染。
这在大多数场景下够用。比如一个用户卡片组件,不同地方用,只是名字、头像、年龄不同,结构完全一样——用 props 传数据就行,没什么问题。
但假设你有一个弹窗组件。
弹窗的边框、遮罩、关闭按钮、动画——这些是固定的,封装成组件合情合理。但弹窗里面的内容呢?有时候是一个表单,有时候是一段提示文字,有时候是一张图片。
这时候 props 就撑不住了。你开始往组件里加各种控制参数:
props: {
showForm: Boolean,
showImage: Boolean,
imageUrl: String,
formType: String,
showConfirmButton: Boolean,
// ... 越加越多
}
写着写着你会发现,这条路越走越窄。props 只能传数据,控制不了结构。你想在弹窗里放一个复杂的自定义排版,props 根本表达不了。
这就是 slot 要解决的问题:让父组件能向子组件注入 UI 结构,而不只是数据。
二、相框的比喻
理解 slot,有一个很直观的比喻:相框。
- 相框本身(边框、材质、尺寸)是固定的 → 子组件负责
- 相框里放什么照片 → 父组件决定
Slot 就是相框中间那个空洞。子组件说:「我这里留了个位置,你来填。」父组件说:「好,我填这个。」
弹窗组件用 slot 改造后:
<!-- 子组件:只管弹窗骨架 -->
<view class="modal">
<view class="modal-header">标题</view>
<view class="modal-body">
<slot></slot> <!-- 这里是洞,内容由父组件填 -->
</view>
</view>
<!-- 父组件 A:填表单 -->
<Modal>
<form>...</form>
</Modal>
<!-- 父组件 B:填图片 -->
<Modal>
<image src="..." />
</Modal>
子组件只有一份,父组件各自填各自的内容,互不干扰。
三、具名插槽:留多个洞
有时候一个组件需要在不同位置留多个洞。比如一个卡片组件,顶部区域、内容区域、底部操作区各不相同。
这时候给每个洞起个名字,父组件按名字对应填入:
<!-- 子组件 -->
<view class="card">
<slot name="header"></slot>
<slot name="body"></slot>
<slot name="footer"></slot>
</view>
<!-- 父组件 -->
<Card>
<template #header>自定义标题区</template>
<template #body>自定义内容区</template>
<template #footer>自定义底部按钮</template>
</Card>
这就是具名插槽,理解起来没什么难度。
四、作用域插槽:有点绕,但有其存在的道理
这是很多人卡住的地方。
前面说的 slot,数据流方向是:父组件 → 子组件(父组件把 UI 结构塞进去)。
但有一种场景,逻辑更复杂一些。
假设你有一个列表组件,它负责循环渲染一组数据。不同页面对每一项的展示要求不一样:有的页面每项显示图文,有的页面每项只显示文字加按钮。
你想用 slot,让父组件自定义每一项的结构。但问题来了:
v-for 的循环发生在子组件内部。父组件只是传入了整个数组,它并不知道"当前循环到哪一项"。
父组件知道:整个 list
子组件知道:当前循环到的 item ← 父组件够不到这个
父组件想自定义渲染,但渲染每一项所需的数据(item)在子组件手里。
作用域插槽就是为了解决这个矛盾——子组件在暴露插槽的同时,把数据一并递出来:
<!-- 子组件:循环时把 item 暴露出去 -->
<view v-for="item in list">
<slot name="item" :item="item"></slot>
</view>
<!-- 父组件:接住 item,自己决定怎么渲染 -->
<List :list="myData">
<template #item="{ item }">
<view>{{ item.title }}</view>
<button>{{ item.actionText }}</button>
</template>
</List>
由此,组件的分工变得很清晰:
- 子组件:负责循环,知道每一项是什么
- 父组件:负责决定每一项渲染成什么样子
五、为什么感觉"绕"?
因为它逆着我们的直觉。
正常的数据流是父 → 子,我们已经习惯了。
作用域插槽的流程是:父把渲染权交给子,子把数据还给父,父用这个数据来渲染。是一种协作关系,不是单向传递。
这个模式转过弯来之后会发现其实很合理——职责分得更清楚了。子组件只管结构和循环,父组件只管内容和展示,两边各司其职,互不越界。
六、总结
用一张表格把三个概念的边界划清楚:
| 解决什么问题 | 传的是什么 | |
|---|---|---|
| Props | 数据复用 | 字符串、数字、对象 |
| Slot | 结构复用 | HTML 标签 + 样式 |
| 作用域插槽 | 结构复用 + 子组件数据 | HTML 标签,同时能用子组件的数据 |
判断要不要用 slot,只问一个问题:
不同地方用这个组件时,差异是数据层面的,还是结构层面的?
数据不同 → Props。
结构不同 → Slot。
结构不同,还需要子组件内部数据 → 作用域插槽。
一句话总结:slot 不是一个复杂的技巧,它解决的是一个很朴素的问题:组件的骨架可以复用,但骨架里填什么,应该由使用者来决定。