Vue slot 插槽用法:具名插槽与作用域插槽传参
tip
v-slot 等同于 v-slot:default
插槽解决的是什么问题
props 能往子组件里传数据,但传不了「一段结构」。当我们写一个弹窗、卡片、表格这类容器组件时,外框的样式和行为是固定的,里面填什么内容却完全由使用方决定。如果只靠 props,就得把所有可能的内容都用配置项穷举出来,组件会越写越臃肿。
插槽(slot)换了个思路:子组件在模板里用 <slot> 挖一个洞占位,父组件在使用这个组件时把要填的模板内容写在标签内部,Vue 在渲染时把内容搬进对应的洞里。这样子组件负责布局,父组件负责内容,职责就分开了。
插槽分三种用法:默认插槽(只有一个洞)、具名插槽(多个洞,靠 name 区分)、作用域插槽(子组件把自己内部的数据反向传给父组件的模板)。第三种最容易绕,因为它的数据流向和 props 是反的。下面依次演示。
简单示例
先看具名插槽。父组件里 <ChildOne> 标签内部写了三块内容:第一块是裸的 <div>,没有指定 name,会落进子组件的默认插槽;后两块用 <template> 包起来并标了 #test 和 v-slot:new,分别落进子组件里 name="test" 和 name="new" 的插槽。# 是 v-slot: 的简写,两种写法完全等价。
父组件
<template>
<div class="text-center">
<ChildOne>
<div>这是第一个 slot 插槽中的内容</div>
<!--<ChildOne />-->
<template #test>
<p>这是名为 test 的插槽</p>
</template>
<template v-slot:new>
<p>这是名为 new 的插槽</p>
</template>
</ChildOne>
</div>
</template>
<script lang="ts" setup>
import ChildOne from './childOne.vue';
</script>
没有 name 的插槽,实际名字就是 default。所以直接写裸标签,和用 <template #default> 显式包一层,效果是一样的。其中 <div>这是第一个 slot 插槽中的内容</div> 可替换为下面的示例
<template #default>
<div>这是第一个 slot 插槽中的内容</div>
</template>
<!-- 或者 -->
<template #>
<div>这是第一个 slot 插槽中的内容</div>
</template>
子组件这边只管挖洞,洞和洞之间可以随意穿插子组件自己的固定内容。父组件传进来的内容会按 name 对号入座,和书写顺序无关。另外 <slot> 标签内部还可以写内容作为兜底:父组件没传对应插槽时显示兜底内容,传了就被整体替换掉。
子组件
<template>
<div class="text-center">
<strong>这是子组件原有的内容,下面是一个插槽</strong>
<slot></slot>
<div>
下面又是一个插槽
<slot name="test"></slot>
</div>
<div>隔开</div>
<div>
这是另一个插槽
<slot name="new"></slot>
</div>
</div>
</template>
插槽中传入参数
上面的用法有个限制:父组件写的模板处在父组件的作用域里,访问不到子组件内部的数据。可是像列表这种场景,循环是子组件在做的,每一项的数据只有子组件手里有,渲染样式却要交给父组件决定。
作用域插槽就是为这个准备的。子组件在 <slot> 标签上像写 props 一样绑定数据(:item="item"),父组件用 v-slot="slotProps" 接收,这些绑定会被打包成一个对象交给父组件的模板。理解的关键是:slotProps 只在这段插槽模板内部可见,它不是父组件的数据,而是子组件每次渲染这个洞时临时传过来的一包参数。子组件循环几次,这段模板就被执行几次,每次 slotProps 里装的是不同那一项。
父组件
<template>
<div class="text-center">
<ChildTwo>
<template #default="slotProps">
<div v-for="(item, index) in slotProps.item" :key="index">
<span>key:{{ item }}</span>
<!--<span>value:{{ slotProps.item[item] }}</span>-->
</div>
{{ slotProps.index }}
{{ slotProps.testStr }}
</template>
</ChildTwo>
<br />
<hr />
<br />
<!-- v-slot:default="slotProps" -->
<ChildTwo v-slot="slotProps">
{{ slotProps.item }}
{{ slotProps.index }}
{{ slotProps.testStr }}
</ChildTwo>
<br />
<hr />
<br />
<ChildThree #test="slotProps">
{{ slotProps.item }}
{{ slotProps.index }}
</ChildThree>
<br />
<hr />
<br />
<ChildThree>
<template #test="slotProps">
{{ slotProps.item }}
{{ slotProps.index }}
</template>
</ChildThree>
<br />
<hr />
<br />
</div>
</template>
<script lang="ts" setup>
import ChildTwo from './childTwo.vue';
import ChildThree from './childThree.vue';
</script>
父组件这段示例里排了四种写法,值得逐个对照:前两种针对默认插槽,<template #default="slotProps"> 是完整写法,<ChildTwo v-slot="slotProps"> 是把 v-slot 直接写在组件标签上的简写,只有在唯一使用默认插槽时才能这么省。后两种针对具名插槽 test,同样是「直接写标签上」和「用 template 包」两种形态。这里有个规则要记住:一旦同时用到多个具名插槽,就必须全部用 <template> 包起来,不能再往组件标签上直接写。
子组件 childTwo.vue 在 v-for 内部放插槽,把当前项 item、下标 index 和一个响应式字符串 testStr 一起传出去。testStr 用 setTimeout 在 1.5 秒后改值,用来验证作用域插槽传出的数据依然是响应式的:值一变,父组件那段插槽模板会跟着重新渲染。
子组件 childTwo.vue
<template>
<div class="text-center">
<div v-for="(item, index) in testArr" :key="index">
<slot :testStr="testStr" :item="item" :index="index"></slot>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
const testArr = reactive([
{ id: '666', name: 'wfd' },
{ id: '888', name: 'fnn' },
{ id: '999', name: 'wfd-fnn' },
]);
const testStr = ref('这是一个测试字符串');
setTimeout(() => {
testStr.value = '更改了测试字符串';
}, 1500);
</script>
childThree.vue 和 childTwo.vue 结构几乎一样,唯一区别是插槽带了 name="test",用来配合父组件里 #test="slotProps" 的两种写法。可以看出具名和传参是两个正交的能力,可以叠加使用。父组件接收时还能直接解构,写成 #test="{ item, index }",模板里就不用每次都带 slotProps. 前缀了。
子组件 childThree.vue
<template>
<div class="text-center">
<div v-for="(item, index) in testArr" :key="index">
<slot name="test" :testStr="testStr" :item="item" :index="index"></slot>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
const testArr = reactive([
{ id: '111', name: 'wfd' },
{ id: '222', name: 'fnn' },
{ id: '333', name: 'wfd-fnn' },
]);
const testStr = ref('这是一个测试字符串');
setTimeout(() => {
testStr.value = '更改了测试字符串';
}, 1500);
</script>

还没有留言,来发表第一条吧~