插槽 (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:所有插槽都是函数,实现父子更新隔离