Vue 插槽:组件通信的“隐形通道”

时间:2024-10-28 19:49:25

在 Vue 中,插槽(slot)是实现组件内容分发的机制,允许我们将子组件的内容传递给父组件,从而提升组件的可复用性和灵活性。插槽的本质是通过将父组件内容传递到子组件指定的插槽位置,使得子组件在渲染时可以动态填充不同的内容。

1. 插槽的类型

Vue 中有几种主要的插槽类型:

1、默认插槽(Default Slot)

默认插槽用于在组件没有指定插槽时,将内容插入到子组件的默认位置。

????:

<!-- 父组件中传递内容 -->
<MyComponent>
  <p>这是默认插槽内容</p>
</MyComponent>

<!-- 子组件模板 -->
<template>
  <div>
    <slot></slot>
  </div>
</template>

2、具名插槽(Named Slot)

具名插槽允许我们通过 name 属性指定插槽名称,以便在父组件中为不同的插槽位置传递内容:

????:

<!-- 父组件中传递具名内容 -->
<MyComponent>
  <template v-slot:header>
    <h1>标题内容</h1>
  </template>
  <template v-slot:footer>
    <p>底部内容</p>
  </template>
</MyComponent>

<!-- 子组件模板 -->
<template>
  <div>
    <slot name="header"></slot>
    <!-- 默认插槽 -->
    <slot></slot>
    <slot name="footer"></slot>
  </div>
</template>

3、作用域插槽(Scoped Slot)

作用域插槽允许子组件向父组件提供数据,从而让父组件可以根据该数据动态渲染插槽内容。适用于一些内容需要根据子组件提供的数据进行渲染的场景。

????:

<!-- 父组件 -->
<MyComponent v-slot:default="{ data }">
  <p>子组件提供的数据:{{ data }}</p>
</MyComponent>

<!-- 子组件模板 -->
<template>
  <slot :data="someData"></slot>
</template>

<script>
export default {
  data() {
    return {
      someData: 'Hello slot'
    }
  }
}
</script>

2. 插槽的本质

插槽的本质是将父组件内容通过编译成 vnode(虚拟节点),然后在子组件渲染时将这些 vnode 放置到子组件的指定位置。Vue 通过 vnode 的编译与插槽的传递,将插槽内容与子组件进行动态绑定,这样可以提升代码复用性,并保持组件内部逻辑的独立性。

当我们使用插槽传递数据时,实际传递的是一个对象 { },每一个插槽对应对象的属性。属性名:插槽名,默认为 default,属性值:函数 function,返回值是虚拟节点。

{
  // 默认插槽
  default: function(){},
  // 具名插槽
  slot1: function(){},
  // 作用域插槽
  slot2: function({ msg}){},
}

因此,传递插槽也就是在传递函数,使用插槽在调用函数,返回虚拟节点。

3. 验证结论

正常使用如下:

App.vue

<template>
  <Comp>
    <p>默认插槽:default slot</p>
    <template #slot1>
      <p>具名插槽:slot1</p>
    </template>
    <template #slot2="{ msg }">
      <p>作用域插槽:{{ msg }}</p>
    </template>
  </Comp>
</template>

<script setup>
import Comp from './components/Comp.vue'
</script>

Comp.vue

<template>
  <div>
    <slot></slot>
    <slot name="slot1"></slot>
    <slot name="slot2" msg="hello slot"></slot>
  </div>
</template>

展示为:

下面使用 JS 实现一下:

Comp.js

import { createElementVNode } from 'vue'
export default {
  setup(_, { slots }) {
    console.log('~ slots:', slots)
    const defaultVNodes = slots.default()
    const slot1VNodes = slots.slot1()
    const slot2VNodes = slots.slot2({ msg: 'hello slot' })
    console.log('~ VNodes', defaultVNodes, slot1VNodes, slot2VNodes)
    return () => {
      return createElementVNode('div', null, [...defaultVNodes, ...slot1VNodes, ...slot2VNodes])
    }
  }
}

或者

import { defineComponent, h } from 'vue'
export default defineComponent({
  name: 'Comp',
  props: {},
  setup(props, { slots }) {
    return () =>
      h('div', [
        slots.default ? slots.default() : h('p', '默认插槽'),
        slots.slot1 ? slots.slot1() : h('p', '具名插槽 slot1'),
        slots.slot2 ? slots.slot2({ msg: 'Hello scoped slot' }) : null
      ])
  }
})

插槽的本质就是在子组件中调用函数,创建虚拟节点显示到页面上。

4. 使用 slot 注意事项

1、避免滥用插槽

插槽虽然可以极大地提升组件的灵活性,但过多的插槽会使组件的 API 变得复杂,不易理解和维护。

2、作用域插槽的命名

为了代码可读性,使用作用域插槽时建议使用有意义的命名,如 v-slot:default="{ item }" 而不是 v-slot="{ data }",这样能让代码的含义更加清晰。

3、默认插槽内容

在设计组件时,可以为插槽设置默认内容,以便在父组件未传递内容时,插槽依旧可以展示一些基本信息。比如:<slot>默认内容</slot>。

4、避免嵌套过深的插槽结构

深层嵌套的插槽结构可能会导致代码可读性差,同时也会增加渲染的复杂度,因此在设计组件时尽量保持插槽的扁平结构。