一文看懂 Vue 编译器里的插槽处理逻辑(buildSlots.ts)

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() 的任务就是:

  1. 找出组件上和 <template> 上的插槽定义;
  2. 把每个插槽变成一个"渲染函数";
  3. 判断插槽是静态的还是动态的;
  4. 最后生成一个统一的 "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-ifv-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 辅助生成,并由作者整理审核。

相关推荐
excel5 小时前
Vue 编译器源码解析:错误系统(errors.ts)
前端
余道各努力,千里自同风5 小时前
uni-app 请求封装
前端·uni-app
excel5 小时前
Vue 编译器核心 AST 类型系统与节点工厂函数详解
前端
excel5 小时前
Vue 编译器核心:baseCompile 源码深度解析
前端
excel5 小时前
Vue 编译核心:transformMemo 源码深度解析
前端
excel5 小时前
Vue 编译核心:transformModel 深度解析
前端
excel5 小时前
Vue 编译器源码精解:transformOnce 的实现与原理解析
前端
前端架构师-老李5 小时前
React中useContext的基本使用和原理解析
前端·javascript·react.js
Moonbit5 小时前
招募进行时 | MoonBit AI : 程序语言 & 大模型
前端·后端·面试