Vue 的"插槽"是组件之间传递内容的桥梁。比如你写了这样的模板:
xml
<MyCard>
<template v-slot:header>标题</template>
<template v-slot:footer>底部</template>
</MyCard>
渲染时,Vue 要把这些内容传给组件内部 <slot name="header">、<slot name="footer"> 的地方。
而在编译阶段,Vue 就要把模板里的插槽结构变成一段"可执行的代码" 。这段工作,正是 buildSlots() 函数负责的。
一、先看整体思路:Vue 编译器要做的事情
在模板编译阶段,Vue 会把模板转成"抽象语法树(AST)",然后一层层地分析。
buildSlots() 的任务就是:
- 找出组件上和
<template>上的插槽定义; - 把每个插槽变成一个"渲染函数";
- 判断插槽是静态的还是动态的;
- 最后生成一个统一的 "slots 对象"------Vue 运行时用它来渲染插槽内容。
所以编译后,大致就会变成这样👇:
css
{
header: () => [createTextVNode('标题')],
footer: () => [createTextVNode('底部')],
_: 1 // 插槽标识,用于优化
}
二、核心逻辑讲解
我们一步步来看它在做什么(你不用太关注每个函数名,只看思路)。
1️⃣ 判断有没有插槽
ini
const onComponentSlot = findDir(node, 'slot', true)
这行意思是:
👉 "看看这个组件本身有没有 v-slot 属性",比如:
ini
<MyCard v-slot="{ msg }">内容</MyCard>
如果有,那它就说明开发者在组件本身定义了一个插槽。
于是 Vue 就会创建一个"默认插槽函数":
less
slotsProperties.push(
createObjectProperty(
arg || createSimpleExpression('default', true),
buildSlotFn(exp, undefined, children, loc),
),
)
翻译成人话就是:
"把插槽名(默认是 default)和渲染函数(children)组成一个对象属性"。
2️⃣ 找出 <template v-slot:name> 的插槽
Vue 允许写在组件内部的 <template v-slot:xxx>,比如:
xml
<MyCard>
<template v-slot:header>标题</template>
<template v-slot:footer>底部</template>
</MyCard>
编译器会循环组件的所有子节点,找到这些 <template>:
c
if (isTemplateNode(slotElement) && (slotDir = findDir(slotElement, 'slot', true))) {
// 获取插槽名和作用域参数
const { arg: slotName, exp: slotProps } = slotDir
const slotFunction = buildSlotFn(slotProps, vFor, slotChildren, slotLoc)
// 把这个插槽函数加到 slots 对象里
slotsProperties.push(createObjectProperty(slotName, slotFunction))
}
简单解释:
slotName→ 插槽名称,比如 "header"、"footer";slotFunction→ 一个生成 VNode 的函数;- 最后组合成
{ header: fn1, footer: fn2 }这样的结构。
3️⃣ 处理带 v-if 或 v-for 的动态插槽
Vue 允许你写:
arduino
<template v-slot:header v-if="showHeader">标题</template>
<template v-slot:item v-for="i in list">Item {{ i }}</template>
这时,编译器要做的就复杂一点:
- 如果有
v-if,编译成"条件表达式"; - 如果有
v-for,编译成"循环渲染函数"。
比如下面这段逻辑:
less
if (vIf = findDir(slotElement, 'if')) {
dynamicSlots.push(
createConditionalExpression(
vIf.exp!,
buildDynamicSlot(slotName, slotFunction, conditionalBranchIndex++),
defaultFallback
)
)
}
可以理解为:
"如果有
v-if,就生成一个条件 ? 有这个插槽 : undefined的表达式。"
而带 v-for 的则会用 RENDER_LIST 来创建循环渲染逻辑。
4️⃣ 处理默认插槽
如果组件没有命名插槽(只有普通子节点),例如:
xml
<MyCard>内容</MyCard>
那就会自动创建一个叫 default 的插槽:
scss
if (!hasTemplateSlots) {
slotsProperties.push(buildDefaultSlotProperty(undefined, children))
}
效果是:
{ default: () => [createTextVNode('内容')] }
5️⃣ 给插槽打上"优化标记"
Vue 编译后会给插槽对象添加一个 _ 属性,用于标识类型:
ini
const slotFlag = hasDynamicSlots
? SlotFlags.DYNAMIC
: hasForwardedSlots(node.children)
? SlotFlags.FORWARDED
: SlotFlags.STABLE
三种情况:
| 标识 | 含义 |
|---|---|
| STABLE (1) | 静态插槽,没变 |
| DYNAMIC (2) | 动态插槽,依赖变量 |
| FORWARDED (3) | 透传插槽(子组件继续传) |
这样 Vue 运行时在 diff 时就能做优化,不用每次都重新比对所有插槽。
三、最后生成的结果长什么样?
比如我们有:
xml
<MyCard>
<template v-slot:header>标题</template>
<template v-slot:footer>底部</template>
</MyCard>
编译后,大致会变成(伪代码):
css
const slots = {
header: () => [createTextVNode('标题')],
footer: () => [createTextVNode('底部')],
_: 1 // STABLE
}
如果有动态条件,就会变成:
php
createSlots(slots, [
showHeader ? { name: 'header', fn: ... } : undefined
])
四、为什么要这样设计?
Vue 3 的编译器比 Vue 2 更灵活,它不再简单拼字符串,而是"构建 AST → 生成函数",这带来了几个好处:
- ✅ 性能更好(静态插槽跳过 diff)
- ✅ 支持复杂条件和循环
- ✅ 插槽作用域变量(
v-slot="{ foo }") 自动追踪 - ✅ 支持嵌套组件 slot 转发
五、总结一句话
buildSlots()的任务,就是"把模板里的各种插槽写法,全部变成一个统一的 slots 对象 ",并在编译阶段就确定哪些是静态的、哪些需要动态生成。
这个对象就是 Vue 运行时渲染插槽的"蓝图"。
🧩 延伸阅读
如果你有兴趣深入了解,可以配合阅读这几个辅助函数:
trackSlotScopes():追踪作用域插槽的变量;trackVForSlotScopes():处理带 v-for 的插槽作用域;buildDynamicSlot():专门用来创建动态插槽结构。
结尾说明:
本文部分内容借助 AI 辅助生成,并由作者整理审核。