今天看代码的时候碰到了插槽,有些看不懂,所以写下这篇文章,系统地梳理一下关于插槽的内容,也希望给大家带来一些帮助。
javascript// 我碰到的插槽长这样 <template #default="scope"> ... </template>
一.什么是插槽?
根据 Vue 官方的定义:"插槽用于向子组件传递内容。和 prop 用于传递数据不同,插槽用于传递更丰富的内容,包括 HTML 元素和其他 Vue 组件。"
简单来说,插槽就像是组件预留的"空位",允许向组件内部插入任何内容,无论是文本、HTML 元素还是其他组件。有了插槽组件更加灵活,可以根据不同的使用场景展示不同的内容。
听起来有点抽象,没关系,举个例子一下就懂了。
现在有一个界面,它是由许多个卡片组件构成的。我们称界面为父组件,卡片组件为子组件,如图:
先看看子组件和父组件的代码:
javascript
// 卡片组件 子组件
<template>
<div class="card">
<slot name="header"></slot>
<slot name="image"></slot>
</div>
</template>
javascript
// 在父组件中使用子组件
<CardComponent>
<template #header>
<h2>vue</h2>
</template>
<template #image>
<img src="vue.jpg" alt="vue">
</template>
</template>
</CardComponent>
<CardComponent>
<template #header>
<h2>react</h2>
</template>
<template #image>
<img src="react.jpg" alt="react">
</template>
</template>
</CardComponent>
// 后两个类似,省略
卡片组件包含标题和图片两部分,但其具体内容是由使用该组件的父组件决定的。那么,父组件如何知道内容应该传递到何处,以及如何进行传递呢?
传递到何处:
子组件通过定义带有特定名称的插槽来预留内容位置。例如,一个用于标题的插槽可以如下表示:
javascript
<slot name="header"></slot>
name="header"
属性定义了一个名为 header
的插槽。
如何传递:
父组件需要将要传递的内容包裹在 <template>
标签内,并通过 #
符号后跟插槽名称来指定内容的插入位置。例如,要将标题传递到名为 header
的插槽,可以这样做:
javascript
<template #header>
<h2>这是卡片标题</h2>
</template>
这样,父组件就能精确地将内容传递到子组件中预定的插槽位置。
二.插槽的种类及其具体使用
理解了插槽是什么后,我们来看看不同类型的插槽。Vue 3 主要有三种插槽:默认插槽、具名插槽、作用域插槽。
1.默认插槽
默认插槽也叫匿名插槽,是最简单的插槽形式。不需要为 <slot>
标签提供 name
属性,并且在使用组件时,也不需要在 <template>
上指定插槽名称。如果一个组件只包含一个插槽,那我们就可以使用默认插槽。
毕竟只有一个坑,不管你给不给这个坑起名字,传进去的萝卜都只能待这个坑。所以没必要给坑起名字,也没必要给要传进去的萝卜指定坑位,一个萝卜一个坑,直接传就行了。
如上图所示,你可以为插槽指定默认内容,就像这样:
javascript
<!-- child.vue -->
<template>
<div>
<slot>默认萝卜</slot>
</div>
</template>
现在,当在父组件中使用 <child>
且没有提供任何插槽内容时,就会渲染"默认萝卜"。
但是,如果父组件提供了插槽内容,那么子组件的默认内容就会被替换掉。
javascript
// 在父组件中使用子组件
<child>我是父组件传递的萝卜,我会取代之前的默认萝卜</child>
父萝卜驾到,通通闪开!
2.具名插槽
具名插槽,顾名思义,具有名字的插槽,也就是带"name"属性的插槽。
<slot>
元素可以有一个特殊的属性name
,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容。
如果我们需要给一个组件的多处地方填充内容,那么具名插槽就有用了。本文刚开始引入的卡片例子,就使用了具名插槽。
本质上是多个萝卜多个坑的问题。要怎么实现把萝卜放进对应的坑里?首先给坑位起名字,然后告诉萝卜,你要去哪个坑,就好了。
再举一个例子:
javascript
// 在父组件中使用子组件
<child>
<template #header>
<h1>我是标题</h1>
</template>
<template #default>
<p>我是第一段话</p>
<p>我是第二段话</p>
</template>
<template #footer>
<p>我是底部信息</p>
</template>
</child>
javascript
// 子组件
<div>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
上面带name属性的就是具名插槽,如果不带name,那么那么它的name
默认为default。
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template>
节点都被隐式地视为默认插槽的内容。
所以上面的父组件也可以写成:
javascript
// 在父组件中使用子组件
<child>
<template #header>
<h1>我是标题</h1>
</template>
<p>我是第一段话</p>
<p>我是第二段话</p>
<template #footer>
<p>我是底部信息</p>
</template>
</child>
如果你要把多个萝卜放入多个坑里,知道自己要去哪个坑的萝卜就会自己找到对应name的坑进去,其他的萝卜都进入默认的坑。
3.作用域插槽
(1)渲染作用域
在了解作用域插槽之前,有必要了解一下"渲染作用域"。
vue官网对"渲染作用域"的描述:
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。举例来说:
javascript<span>{{ message }}</span> <FancyButton>{{ message }}</FancyButton>
这里的两个
{``{ message }}
插值表达式渲染的内容都是一样的。插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。换言之:
父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。
在 Vue 中,组件的作用域规则是这样的:
- 父组件模板中的所有内容都是在父组件的作用域中编译的。在父组件模板中,可以访问父组件的数据和方法。
- 子组件模板中的所有内容都是在子组件的作用域中编译的。在子组件模板中,可以访问子组件的数据和方法。
插槽内容也是如此:
- 当在父组件中使用子组件并定义插槽内容时,这些插槽内容是在父组件的作用域中编译的。因此,可以在插槽内容中使用父组件的数据和方法,但不能直接使用子组件的数据和方法。
- 子组件可以通过
props
接收来自父组件的数据,但是插槽内容本身不能直接访问子组件的数据。
那如果我们需要在插槽内容中使用子组件的数据呢?
那就要用到"作用域插槽"了。
(2)默认作用域插槽
在子组件slot
标签绑定属性,然后在父组件中用v-slot进行接收,就可以把数据传递给父组件中。
在接收方式上,默认插槽和具名插槽有一些不同,先来看看默认插槽如何接收子组件传过来的props:
javascript
//子组件
<template>
<div>
<slot personName="清清" age="3"></slot>
</div>
</template>
//父组件
<template>
<div>
<Child v-slot="slotProps">
我是 {{ slotProps.personName }} ,我今年 {{ slotProps.age }} 岁了。
</Child>
</div>
</template>
<script setup>
import Child from './Child.vue'
</script>
这里的slotProps是插槽 prop 的名字,可以自定义,方便后面使用。
也可以接收响应式数据:
(3)具名作用域插槽
具名作用域插槽允许给插槽起个名字,并且可以通过 v-slot:插槽名字="slotProps"
的方式来接收子组件传递的数据。如果用缩写的话,可以写成 #插槽名字="slotProps"
。
javascript
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
子组件传递数据给插槽::
javascript
<slot name="header" message="hello"></slot>
插槽的 name
是 Vue 保留的属性,不会作为数据传递给插槽。所以,父组件拿到的headerProps
,它只会包含 { message: 'hello' }
。
(4)注意事项
如果同时使用了具名插槽和默认插槽,那么需要为默认插槽明确地使用 <template>
标签。如果不这样做,可能会出现编译错误,因为这样容易混淆哪个数据属于哪个插槽。
举个例子,有一个组件 MyComponent
,它有一个默认插槽和一个名为 footer
的具名插槽,我们不能直接在组件上使用 v-slot
指令,这样会导致数据的作用域不清楚。
所以,我们应该这样做:
javascript
<MyComponent>
<!-- 明确地使用默认插槽 -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>
<!-- 具名插槽 -->
<template #footer>
<p>这里有一些联系信息</p>
</template>
</MyComponent>
这样的话, message
只在默认插槽中可用,而在 footer
插槽中不可用。这样代码更清晰,也更不容易出错。
通过上述例子,我们可以看到插槽在 Vue 3 中的强大之处。它们不仅使组件更加灵活,还极大地提高了组件的可复用性。