◆

Vue slot 插槽用法:具名插槽与作用域插槽传参

2023-02-02 01:370

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>
💬留言板0

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

PAGE LOAD / LIVEVue slot 插槽用法:具名插槽与作用域插槽传参加载中