Vue SSR 源码解析:ssrTransformSuspense 与 ssrProcessSuspense

一、背景与概念说明

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) 」任务:

  1. 检测该组件是否有子节点;
  2. 创建一个 WIPEntry 存入全局的 wipMap
  3. 调用 buildSlots 构造 slots 的表达式;
  4. 为每个 slot 生成一个函数表达式 fn
  5. 暂时不填充函数体(稍后在 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(抽象表示),现在要:

  1. 填充每个 slot 的函数体;
  2. 输出最终的 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 代码语句

五、实践层:执行链分析

  1. 模板解析时遇到 <Suspense>
  2. 触发 ssrTransformSuspense
  3. 暂存 slot 信息;
  4. 所有模板节点 transform 完成后;
  5. 进入 codegen 阶段;
  6. 调用 ssrProcessSuspense
  7. 输出最终可执行 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

八、总结

ssrTransformSuspensessrProcessSuspense 共同完成了 <Suspense> 的 SSR 编译:

  • 前者负责收集 slot 信息;
  • 后者负责生成最终的服务端渲染调用;
  • 通过两阶段机制实现异步安全的模板渲染逻辑。

这套机制充分展示了 Vue SSR 编译器中对「异步组件渲染」的精巧设计。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

相关推荐
崔庆才丨静觅14 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅15 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅15 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment15 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅16 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊16 小时前
jwt介绍
前端
爱敲代码的小鱼16 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte16 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
NEXT0616 小时前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法
剪刀石头布啊16 小时前
生成随机数,Math.random的使用
前端