在 Vue 3 的 Composition API 中,插槽(Slot)作为组件间内容分发的重要机制,其使用方式与 Vue 2 相比有了显著变化。特别是在 <script setup> 语法糖环境下,我们需要理解几个核心概念:
虚拟 DOM 与真实 DOM 的区别:Vue 的渲染流程是先创建虚拟节点(VNode),再将其转换为真实 DOM。useSlots() 获取的是 VNode 而非真实 DOM,这是很多开发者容易混淆的关键点。
渲染时机的差异:在 setup 阶段(即 <script setup> 执行时),组件尚未挂载,此时无法获取真实 DOM。必须在 mounted 生命周期后才能操作 DOM 元素。
作用域隔离:子组件内部的 ref 无法直接获取父组件传递的插槽内容 DOM,这是 Vue 刻意设计的隔离机制,需要通过特定方式突破这种隔离。
这是最基础的插槽访问方式,适用于需要基于插槽内容做逻辑判断的场景:
javascript复制<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
// 检查默认插槽是否存在内容
const hasDefaultSlot = !!slots.default
// 获取具名插槽内容
const footerSlot = slots.footer?.() || []
// 高级用法:动态渲染插槽
const renderDynamicSlot = (name) => {
return slots[name]?.() || h('div', 'Fallback Content')
}
</script>
重要提示:
slots.default()返回的是 VNode 数组,即使只有一个子节点也是如此。如果需要对单个节点操作,需要手动处理数组。
当需要在父组件中访问插槽 DOM 时,属性透传是最简洁的方案:
vue复制<!-- ChildComponent.vue -->
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
<template>
<div class="child-wrapper">
<slot v-bind="attrs"></slot>
</div>
</template>
<!-- ParentComponent.vue -->
<template>
<ChildComponent ref="childRef">
<div class="slot-content" ref="contentRef">
<!-- 这里的内容会被透传 -->
</div>
</ChildComponent>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const contentRef = ref(null)
onMounted(() => {
console.log(contentRef.value) // 正确获取到插槽DOM
})
</script>
这种方式的优势在于:
对于需要完全控制插槽渲染流程的场景,可以采用高阶组件模式:
javascript复制// createSlotWrapper.js
import { h, defineComponent } from 'vue'
export function createSlotWrapper(options = {}) {
return defineComponent({
props: ['vnode'],
mounted() {
options.onMounted?.(this.$el)
},
render() {
return this.vnode
? h('div', { class: options.wrapperClass }, [this.vnode])
: h('div', options.fallback || 'No slot content')
}
})
}
使用示例:
vue复制<script setup>
import { createSlotWrapper } from './createSlotWrapper'
const SlotWrapper = createSlotWrapper({
wrapperClass: 'slot-container',
onMounted: (el) => {
console.log('DOM ready:', el)
// 可以在这里进行DOM操作
}
})
</script>
<template>
<SlotWrapper :vnode="$slots.default?.()[0]" />
</template>
结合 MutationObserver 实现动态内容监听:
javascript复制<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const slotContainer = ref(null)
let observer = null
onMounted(() => {
observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log('Slot content changed:', mutation.addedNodes)
})
})
if (slotContainer.value) {
observer.observe(slotContainer.value, {
childList: true,
subtree: true
})
}
})
onUnmounted(() => {
observer?.disconnect()
})
</script>
<template>
<div ref="slotContainer">
<slot></slot>
</div>
</template>
插槽内容未更新:
Ref 获取不到 DOM:
SSR 兼容性问题:
避免不必要的插槽解析:
javascript复制// 不好的做法:每次渲染都解析插槽
const slotContent = computed(() => slots.default?.())
// 好的做法:只在需要时解析
function getSlotContent() {
return slots.default?.()
}
合理使用 keep-alive:
vue复制<template>
<keep-alive>
<slot v-if="shouldKeepAlive"></slot>
</keep-alive>
<slot v-else></slot>
</template>
批量 DOM 操作:
javascript复制function batchUpdate() {
const el = slotContainer.value
if (!el) return
requestAnimationFrame(() => {
// 在这里集中进行DOM操作
})
}
实现一个可以动态转发插槽的组件:
vue复制<!-- SlotProxy.vue -->
<script setup>
defineProps(['name'])
const slots = useSlots()
const resolveSlot = (name) => {
return slots[name] || slots.default
}
</script>
<template>
<slot :name="name" v-if="$slots[name]"></slot>
<slot v-else></slot>
</template>
通过 provide/inject 扩展插槽作用域:
javascript复制// 在父组件中
provide('slotContext', {
theme: 'dark',
locale: 'zh-CN'
})
// 在插槽内容中
const context = inject('slotContext')
typescript复制defineSlots<{
default(props: { clickHandler: () => void }): any
header?: (props: { title: string }) => any
footer?: (props: { copyright: string }) => any
}>()
经过多个项目的实践验证,我总结出以下插槽使用原则:
简单优先原则:能用模板语法解决的问题,就不要使用 useSlots()
关注点分离:DOM 操作逻辑尽量放在父组件中
性能意识:避免在渲染函数中频繁解析插槽
类型安全:TypeScript 项目务必定义插槽类型
可测试性:为包含复杂插槽逻辑的组件编写单元测试
实际项目中,我通常会创建一个 slotUtils.js 工具文件,包含常用的插槽处理函数,如:
javascript复制export function getFirstSlotNode(slots, name = 'default') {
const slot = slots[name]
return slot?.()[0]
}
export function hasSlotContent(slots, name = 'default') {
return !!slots[name]
}
export function renderSlotWithFallback(slots, name, fallback) {
return hasSlotContent(slots, name)
? slots[name]()
: fallback
}
这些工具函数可以显著提升代码的可维护性。最后要强调的是,虽然 Vue 3 提供了强大的插槽操作能力,但应该谨慎使用这些底层 API,大多数场景下模板语法已经足够。