💬 聊聊Vue2.x插槽那些事

1,976 阅读3分钟

Vue插槽的主要作用是承载分发内容的出口,分为:匿名插槽、具名插槽以及作用域插槽3种

举个"栗子",创建以下文件:

  • child.vue:封装 name 为 ChildExample 的组件

  • parent.vue:引入 child.vue,调用 ChildExample 组件

匿名插槽

<slot> </slot>是最常用的插槽,根据需要自定义内容

// child.vue
<slot></slot>

// parent.vue
<child-example>
  <span>这是匿名插槽的内容</span>
</child-example>

具名插槽

假设一个插槽满足不了需求,而多个插槽的设置,赋上 name 值是必要的喔~

注意:匿名插槽是特殊的具名插槽,其 name 为 default,避免同名!

<template> + v-slot指令(2.6.0+),或<slot> + name属性

// child.vue
<slot name="left"></slot>
<slot name="right"></slot>

// parent.vue
<child-example>
  <span slot="left">这是具名插槽left的内容</span>

  <!-- 2.6.0 + -->
  <template v-slot:right>
    <span>这是具名插槽right的内容</span>
  </template>
</child-example>

作用域插槽

这个插槽可以访问到子组件中的数据,为内容分发实现更多可能性~ 推荐v-slot:slotName="slotProps"的写法,替代了slot + slot-scope

// child.vue
<slot name="demo" :data="list" :param="option"></slot>

// parent.vue
<child-example>
  <!-- 2.6.0 + -->
  <template v-slot:demo="{data, param }">
    <span>{{data}}{{param}}</span>
  </template>

  <!-- 或slot+slot-scope -->
  <!-- <span slot="demo" slot-scope="someProps">{{someProps.data}}{{someProps.param}}</span>  -->
</child-example>

⚠️ 请注意

  1. slot 和 slot-scope 在 Vue 2.x 版本仍被支持,但已被官方废弃且不出现在 Vue 3,推荐使用v-slot指令

  2. v-slot 指令只能加在template自定义组件

当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用,即可把 v-slot 直接用在组件上独占默认插槽的缩写语法

  1. 插槽 name 一致的 v-slot 会 覆盖 slot

  2. 插槽 name 一致的插槽内容,后者覆盖前者

  3. 插槽编译作用域: 编译作用域

    • 父级模板里的所有内容都是在父级作用域中编译的;

    • 子模板里的所有内容都是在子作用域中编译的

$slots & $scopedSlots

与插槽相关的两个API,可知官方说明概况如下:

使用$slots$scopedSlots封装组件

封装一个待办事项组件:在todoList.vue封装,调用在demo.vue

方法一

暂不利用插槽,todoList.vue如下:

<template>
  <ul>
    <li v-for="(item, index) in list" :key="index">
      <span>{{ item.date }} :</span>
      {{ item.thing }}
    </li>
  </ul>
</template>

<script>
export default {
  name: "todoList",
  props: {
    list: {
      type: Array,
      default() {
        return [];
      }
    }
  }
};
</script>

在demo.vue引入todoList.vue,并调用:

<template>
  <div id="demo">
    <todo-list :list="data"></todo-list>
  </div>
</template>
import todoList from "src/components/todoList";
export default {
  name: "Demo",
  components: {todoList},
  data() {
    return {
      data: [
        { date: "Monday", thing: "Working" },
        { date: "Tuesday", thing: "Working" },
        { date: "Wednesday", thing: "Working" },
        { date: "Thursday", thing: "Working" },
        { date: "Friday", thing: "Working" },
        { date: "Saturday", thing: "Vacation" },
        { date: "Sunday", thing: "Vacation" }
      ],
    };
  }
};

效果-->


方法二

换一种实现方法:访问$slots(已知:访问$slots有利于使用渲染函数编写组件)

根据数据,可知需要设置7个插槽,其name对应取date值。在此,补充一个知识点:

v-slot 支持 动态插槽名,对应语法:v-slot:[dynamicSlotName]

所以,todoList.vue改为:

<template>
  <todoItem></todoItem>
</template>

<script>
export default {
  name: "todoList",
  components: {
    todoItem: {
      render(h) {
        let root = this.$parent;
        const {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday} = root.$slots;
        return h("ul", [
          h("li", Monday),
          h("li", Tuesday),
          h("li", Wednesday),
          h("li", Thursday),
          h("li", Friday),
          h("li", Saturday),
          h("li", Sunday)
        ]);
      }
    }
  }
};
</script>

对应demo.vue调用改为:

<todo-list>
  <template v-for="(item, index) in data" v-slot:[item.date]>
    <p :key="index">
      <span>{{ item.date }} --- </span>
      <span style="color:#aaa">{{ item.thing }}</span>
    </p>
  </template>
</todo-list>

效果-->


方法三

实际上,方法二这样编写并不常见,更多的场景,是像Element-Tree组件,根据需要,既可使用默认树节点样式,也可自定义节点内容。

待办事项组件也想如此,假设待办数据中周末两日需自定义内容,可以使用作用域插槽来实现效果:

改变数据,用 slotName 属性指定目标插槽名称

{ date: "Saturday", thing: "Vacation", slotName: "weekend" },
{ date: "Sunday", thing: "Vacation", slotName: "weekend" }

相对于方法一,demo.vue添加编写目标插槽(weekend)

<todo-list :list="data2">
  <template v-slot:weekend="item">
    <p style="color:#ff9948">
      <span>{{ item.date }} --- </span>{{ item.thing }}!!! Have a good day!
    </p>
  </template>
</todo-list>

todoList.vue则需要对slotName属性进行判断:若存在slotName属性,则渲染为通过访问 $scopedSlots 获取到自定义的插槽内容


<template>
  <ul>
    <li v-for="(item, index) in list" :key="index">
      <!-- 无slotName -->
      <template v-if="!item.slotName">
        <span>{{ item.date }} :</span>
        {{ item.thing }}
      </template>
      <!-- 存在slotName -->
      <scopeItem v-else :option="item"></scopeItem>
    </li>
  </ul>
</template>

<script>
export default {
  name: "todoList",
  components: {
    scopeItem: {
      props: {
        option: { required: true }
      },
      render(h) {
        const {root: $parent, option} = this;
        return root.$scopedSlots[option.slotName](option);
      }
    }
  },
  props: {
    list: {
      type: Array,
      default() {
        return [];
      }
    }
  }
};
</script>

效果-->

为什么可以root.$scopedSlots[option.slotName](option)(调用$scopedSlots.weekend)?

console一下,输出可知其类型为 function

Last but not least

如有不妥,请多指教~