插槽 (Slots)

让组件更灵活

三种插槽

默认插槽

父组件的内容放到子组件的 <slot /> 位置。

<!-- Child.vue -->
<template>
  <div class="card">
    <slot />
  </div>
</template>
<!-- Parent.vue -->
<Child>
  <p>这是内容</p>
</Child>

具名插槽

多个分发点时用(Header、Main、Footer):

<!-- Child.vue -->
<template>
  <header><slot name="header" /></header>
  <main><slot /></main>
  <footer><slot name="footer" /></footer>
</template>
<!-- Parent.vue -->
<Child>
  <template #header>标题</template>
  <template #default>内容</template>
  <template #footer>底部</template>
</Child>

作用域插槽

数据反向传递:让父组件能访问子组件的数据。

<!-- Child.vue -->
<script setup>
const list = ['a', 'b', 'c']
</script>

<template>
  <div v-for="item in list" :key="item">
    <slot :item="item" :index="$index" />
  </div>
</template>
<!-- Parent.vue -->
<Child v-slot="{ item, index }">
  {{ index }} - {{ item }}
</Child>

实现原理

核心概念

本质:父组件向子组件传一段模板(VNode),子组件在指定位置渲染。

  • 父组件写 <Child>内容</Child>,内容编译成 VNode,传给子组件
  • 子组件 <slot /> 位置渲染这些 VNode
  • 作用域插槽:子组件调用插槽函数时传入参数,父组件就能访问子组件数据

Vue 2

普通插槽

父组件模板中的插槽内容,在父组件编译时就解析成 VNode:

// 父组件编译后的 render
render() {
  const childVNode = h(Child, null, [
    h('p', '这是内容') // 父组件的 VNode
  ])
}

子组件的 <slot /> 渲染时,直接使用父组件传进来的 VNode。

问题:父组件更新 → 重新生成 VNode → 子组件被迫更新

作用域插槽

编译成函数,在子组件渲染时才调用:

// 子组件编译后
render() {
  const slot = this.$scopedSlots.default
  const slotVNodes = slot({ item: 'a' }) // 调用函数,传入子组件数据
  return h('div', slotVNodes)
}

这样父组件更新不会影响子组件,但还是有依赖。

Vue 3

所有插槽都变成函数,实现真正的父子更新隔离:

// 子组件的 render
import { useSlots } from 'vue'

setup(props, { slots }) {
  const slot = slots.default // 这是一个函数!

  return () => {
    // 只有当子组件渲染 slot 时,才调用函数
    return h('div', slot({ item: 'a' }))
  }
}

更新隔离

┌─────────────────────────────────────────────┐ │ 父组件更新 │ │ count++ → 父组件 render → 父组件 DOM 更新 │ └─────────────────────────────────────────────┘ │ │ 不影响! ▼ ┌─────────────────────────────────────────────┐ │ 子组件 │ │ 插槽函数未调用 → 子组件不重新渲染 │ └─────────────────────────────────────────────┘

Vue 3 的优势

  • 父组件更新 → 只渲染父组件
  • 子组件数据变化 → 只触发插槽函数 → 子组件更新
  • 真正实现了父子组件更新隔离

动态插槽名

<template #[slotName]>内容</template>

<script setup>
const slotName = 'header'
</script>

总结

插槽作用
默认放内容到子组件
具名多个分发点
作用域数据反向传递

Vue 2 vs Vue 3

  • Vue 2:普通插槽在父组件编译时解析,作用域插槽是函数
  • Vue 3:所有插槽都是函数,实现父子更新隔离