Skip to content

插槽

与 react 的 children 类似,vue 可以使用插槽进行模板片段的传递

img

通过使用插槽,<FancyButton> 仅负责渲染外层的 <button> (以及相应的样式),而其内部的内容由父组件提供。

父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。

默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个 <SubmitButton> 组件:

vue
<button type="submit">
  <slot>
    Submit <!-- 默认内容 -->
  </slot>
</button>

当我们在父组件中使用 <SubmitButton> 且没有提供任何插槽内容时, “Submit”将会被作为默认内容渲染。

具名插槽

当我们需要多个插槽时,对于这种场景,<slot> 元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:

vue
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

这类带 name 的插槽被称为具名插槽 (named slots)。没有提供 name<slot> 出口会隐式地命名为“default”。

要为具名插槽传入内容,我们需要使用一个含 v-slot 指令的 <template> 元素,并将目标插槽的名字传给该指令:

vue
<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- 隐式的默认插槽 相当于 <template #default> -->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。

img

动态插槽名

动态指令参数在 v-slot 上也是有效的,即可以定义下面这样的动态插槽名:

vue
<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

注意,这里的表达式和动态指令参数受相同的语法限制。

作用域插槽

在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。

要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 props 对象:

vue
<template>
    <v-button @click="onClick">
        <slot :text="text" :count="count"></slot>
    </v-button>
</template>
<script lang='ts' setup>
import { ref } from "vue"

const text = ref("base btn")
const count = ref(0)

const onClick = () => {
    text.value = count.value % 2 === 0 ? "BASE BTN" : "base btn"
    count.value++
}
</script>
vue
<BaseBtn v-slot="{ text, count }">
  {{ text }} {{ count }}
</BaseBtn>

注意插槽上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。

具名作用域插槽

如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template> 标签。

vue
<template>
  <MyComponent>
    <!-- 使用显式的默认插槽 -->
    <template #default="{ message }">
      <p>{{ message }}</p>
    </template>

    <template #footer>
      <p>Here's some contact info</p>
    </template>
  </MyComponent>
</template>

如果尝试直接为组件添加 v-slot 指令将导致编译错误。这是为了避免因默认插槽的 props 的作用域而困惑。

无渲染组件

一些组件可能只包括了逻辑而不需要自己渲染内容,视图输出通过作用域插槽全权交给了消费者组件。

这里有一个无渲染组件的例子,一个封装了追踪当前鼠标位置逻辑的组件:

App.vue

vue
<script setup>
import MouseTracker from './MouseTracker.vue'
</script>

<template>
	<MouseTracker v-slot="{ x, y }">
  	Mouse is at: {{ x }}, {{ y }}
	</MouseTracker>
</template>

MouseTracker.vue

vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
  
const x = ref(0)
const y = ref(0)

const update = e => {
  x.value = e.pageX
  y.value = e.pageY
}

onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

<template>
  <slot :x="x" :y="y"/>
</template>

虽然这个模式很有趣,但大部分能用无渲染组件实现的功能都可以通过组合式 API 以另一种更高效的方式实现,并且还不会带来额外组件嵌套的开销。