一、背景与概念说明
Vue 在服务端渲染(SSR)过程中,会对组件模板进行两阶段编译:
- 阶段一(Transform) :生成用于描述结构的中间表达(IR, Intermediate Representation)。
- 阶段二(Codegen) :将中间表达转换为最终的字符串拼接指令(例如
_push、_renderSlot)。
而 <Suspense> 组件是 Vue 3 的一个特殊机制,用于异步内容加载与占位渲染。
在 SSR 环境下,Vue 需要为 <Suspense> 生成可在服务端正确处理异步与 fallback(回退内容)的渲染逻辑。
二、源码概览
typescript
import {
type ComponentNode,
type FunctionExpression,
type SlotsExpression,
type TemplateChildNode,
type TransformContext,
buildSlots,
createCallExpression,
createFunctionExpression,
} from '@vue/compiler-dom'
import {
type SSRTransformContext,
processChildrenAsStatement,
} from '../ssrCodegenTransform'
import { SSR_RENDER_SUSPENSE } from '../runtimeHelpers'
const wipMap = new WeakMap<ComponentNode, WIPEntry>()
interface WIPEntry {
slotsExp: SlotsExpression
wipSlots: Array<{
fn: FunctionExpression
children: TemplateChildNode[]
}>
}
🔍 概念层拆解
-
ComponentNode:表示模板中的组件节点(如<Suspense>)。 -
WeakMap<ComponentNode, WIPEntry>:用于临时保存「正在处理中(work in progress)」的 Suspense 组件信息。 -
WIPEntry:存放两个关键内容:slotsExp: 代表 Suspense 的 slots 表达式(通过buildSlots生成)wipSlots: 存储每个 slot 的函数与对应的子节点列表
三、阶段一:ssrTransformSuspense
javascript
export function ssrTransformSuspense(
node: ComponentNode,
context: TransformContext,
) {
return (): void => {
if (node.children.length) {
const wipEntry: WIPEntry = {
slotsExp: null!, // to be immediately set
wipSlots: [],
}
wipMap.set(node, wipEntry)
wipEntry.slotsExp = buildSlots(
node,
context,
(_props, _vForExp, children, loc) => {
const fn = createFunctionExpression(
[],
undefined, // no return, assign body later
true, // newline
false, // suspense slots are not treated as normal slots
loc,
)
wipEntry.wipSlots.push({
fn,
children,
})
return fn
},
).slots
}
}
}
🧠 原理层说明
此函数完成「第一阶段(transform) 」任务:
- 检测该组件是否有子节点;
- 创建一个
WIPEntry存入全局的wipMap; - 调用
buildSlots构造 slots 的表达式; - 为每个 slot 生成一个函数表达式
fn; - 暂时不填充函数体(稍后在 phase 2 中完成);
💬 逐行解析
-
wipMap.set(node, wipEntry):标记当前<Suspense>节点正在处理中; -
buildSlots(...):解析模板中<template #default>与<template #fallback>之类的内容; -
createFunctionExpression(...):- 参数为空;
undefined表示暂不生成函数体;true表示函数体换行;false表示这是特殊 slot(Suspense 专用);
-
将生成的
fn与对应的子节点children存入wipSlots; -
返回
fn,最终形成 slots 的对象表达式。
⚖️ 对比分析
| 场景 | 普通组件 | <Suspense> 组件 |
|---|---|---|
| Slot 生成函数 | 同步生成并立即填充 | 延迟填充(分两阶段) |
| Transform 阶段 | 完成全部处理 | 仅建立 WIP 结构 |
四、阶段二:ssrProcessSuspense
ini
export function ssrProcessSuspense(
node: ComponentNode,
context: SSRTransformContext,
): void {
const wipEntry = wipMap.get(node)
if (!wipEntry) {
return
}
const { slotsExp, wipSlots } = wipEntry
for (let i = 0; i < wipSlots.length; i++) {
const slot = wipSlots[i]
slot.fn.body = processChildrenAsStatement(slot, context)
}
context.pushStatement(
createCallExpression(context.helper(SSR_RENDER_SUSPENSE), [
`_push`,
slotsExp,
]),
)
}
🧩 原理层说明
这是 第二阶段(codegen) 的入口。
此时模板节点已被转换为结构化 IR(抽象表示),现在要:
- 填充每个 slot 的函数体;
- 输出最终的 SSR 渲染调用。
💬 逐步解析
wipMap.get(node):获取上阶段保存的临时状态;processChildrenAsStatement(slot, context):将 slot 子节点转换为可执行的 SSR 语句;slot.fn.body = ...:补全上阶段未填充的函数体;context.pushStatement(...):生成_push(ssrRenderSuspense(slots))调用。
最终生成的 SSR 代码结构大致如下:
less
_push(ssrRenderSuspense(_push, {
default: () => { /* render main content */ },
fallback: () => { /* render fallback */ }
}))
🧭 逻辑对比
| 阶段 | 输入 | 输出 |
|---|---|---|
| Phase 1 | AST + TransformContext | WIPEntry(未完成函数) |
| Phase 2 | WIPEntry + SSRContext | 完整 SSR 代码语句 |
五、实践层:执行链分析
- 模板解析时遇到
<Suspense>; - 触发
ssrTransformSuspense; - 暂存 slot 信息;
- 所有模板节点 transform 完成后;
- 进入 codegen 阶段;
- 调用
ssrProcessSuspense; - 输出最终可执行 SSR 渲染函数。
这两阶段对应 Vue 编译流程的 "延迟处理机制" ,即在 transform 阶段只建立依赖关系,在 codegen 阶段再填充内容。
六、拓展与思考
1️⃣ 为什么使用 WeakMap
WeakMap 用于存储临时数据,不会影响垃圾回收(避免内存泄漏)。
每个节点对应的 WIPEntry 在生成代码后可被回收。
2️⃣ 为什么分两阶段
Suspense 的 children 可能包含异步或嵌套结构,无法在一次 transform 中立即处理完,因此拆成两个阶段以确保:
- Slot 函数能在后续拿到完整子节点;
- SSR 生成顺序保持一致。
3️⃣ 编译器设计哲学
这种设计体现了 Vue 编译器的「延迟求值 」思想------
在 transform 阶段尽量只收集结构信息,在 codegen 阶段集中生成逻辑表达式。
七、潜在问题与调试建议
| 问题类型 | 可能原因 | 解决思路 |
|---|---|---|
| Suspense 不渲染 fallback | wipSlots 未正确填充 |
检查 transform 阶段是否被提前清理 |
| SSR 输出不正确 | context.helper 注册缺失 |
确认 SSR_RENDER_SUSPENSE 已导入 |
| 内存溢出 | 未清理 wipMap | 确认编译流程末尾自动 GC |
八、总结
ssrTransformSuspense 与 ssrProcessSuspense 共同完成了 <Suspense> 的 SSR 编译:
- 前者负责收集 slot 信息;
- 后者负责生成最终的服务端渲染调用;
- 通过两阶段机制实现异步安全的模板渲染逻辑。
这套机制充分展示了 Vue SSR 编译器中对「异步组件渲染」的精巧设计。
本文部分内容借助 AI 辅助生成,并由作者整理审核。