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 辅助生成,并由作者整理审核。

相关推荐
excel2 小时前
Vue SSR 编译阶段中的 ssrInjectCssVars 深度解析
前端
excel2 小时前
Vue SSR 组件转换源码深度解析:ssrTransformComponent.ts
前端
excel2 小时前
Vue SSR 编译机制解析:ssrTransformSlotOutlet 与 ssrProcessSlotOutlet
前端
顾安r3 小时前
11.8 脚本网页 推箱子
linux·前端·javascript·flask
玖釉-4 小时前
用 Vue + DeepSeek 打造一个智能聊天网站(完整前后端项目开源)
前端·javascript·vue.js
编程社区管理员11 小时前
React 发送短信验证码和验证码校验功能组件
前端·javascript·react.js
全马必破三11 小时前
React“组件即函数”
前端·javascript·react.js
三思而后行,慎承诺11 小时前
React 底层原理
前端·react.js·前端框架
座山雕~11 小时前
html 和css基础常用的标签和样式
前端·css·html