先记住这个答案
Vue 插槽由两部分协作:父组件提供内容,因此其中的表达式在父级作用域编译并读取父级状态;子组件用 <slot> 决定内容插入自己模板的哪个位置。<slot> 标签内部的后备内容只在父级没有提供对应插槽时渲染。父级显式提供一个返回空内容或受条件控制为空的插槽,和完全没有提供插槽并不总是同一情况,组件若依赖是否存在,应通过 $slots/default 或 useSlots 检查插槽函数,再根据产品语义处理空结果。
- 插槽表达式读取父级作用域
- 子级出口决定结构位置
- 未提供插槽时才使用后备内容
内容所有权决定作用域
父模板写 {{ user.name }} 时,user 必须存在于父级,即使这段标记最终出现在子组件内部。这样父级不用知道子组件实例状态,重构子组件内部实现也不会悄悄改变表达式的变量来源。
子组件若要把自己的 item 或状态交给插槽,应显式传成 slot props,由父级接收,这就是作用域插槽。不能因为最终 DOM 位于子组件根节点下,就尝试直接读取子级局部变量。
后备内容只覆盖未提供的通道
下例 Panel 调用 slots.default;没有默认插槽时渲染“暂无内容”,提供插槽时渲染父级返回的节点。渲染函数写法与模板 <slot>暂无内容</slot> 表达同一契约。
如果父级提供函数但函数因 v-if 返回空数组,slots.default 已存在,框架不会把它简单视为“没有提供”。设计空状态时更可靠的方式是让父级决定是否传内容,或传入明确的 empty prop,而不是遍历 VNode 猜测视觉上是否为空。
import { defineComponent, h } from 'vue'
const Panel = defineComponent({
setup(_props, { slots }) {
return () => h('section', { class: 'panel' },
slots.default ? slots.default() : [h('p', '暂无内容')]
)
}
})
const WithContent = () => h(Panel, null, {
default: () => h('strong', '来自父级')
})挂载 Panel 本身会显示后备段落;挂载 WithContent 会显示父级提供的 strong。插槽函数在渲染期间调用,从父级闭包读取状态。
插槽函数应在渲染上下文中调用
Vue 会跟踪插槽函数所读取的父级响应式依赖,并让子级决定调用位置。把 slots.default() 的结果长期缓存到 setup 变量会把 VNode 与旧依赖快照固定下来,可能错过后续状态变化。
需要判断插槽是否存在时可以使用 useSlots,但它不是普通业务状态,通常不必放进 watch。API 设计应尽量让结构选择由模板表达,只有组件布局确实依赖插槽存在性时才读取 slots。
容易答错的地方
- 以为插槽内容能直接访问子级 data
- 内容属于父级模板作用域,只能读取父级变量。需要子数据时由子级通过 slot props 显式传出。
- 把空插槽和缺失插槽一律当成相同
- 提供的插槽函数可能因条件返回空节点。若空状态影响业务,应定义显式契约,而不是只检查渲染结果的表面长度。
面试官还会怎么问?
默认插槽可以和具名插槽一起用吗?
可以。未写名称的内容进入 default,template #header 等进入具名通道;子组件为每个出口分别决定后备内容。
为什么插槽函数不建议在 setup 顶层调用?
setup 顶层不是组件渲染依赖收集的正常位置,得到的 VNode 也不应跨渲染重复使用。应在返回的渲染函数或模板中调用。
后备内容能读取子组件状态吗?
能,因为后备内容写在子组件模板中,由子组件编译。只有父级提供的替换内容受父级作用域规则约束。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。