Vue Slot 到底在解决什么问题?

51 阅读4分钟

很多人学 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 不是一个复杂的技巧,它解决的是一个很朴素的问题:组件的骨架可以复用,但骨架里填什么,应该由使用者来决定。