普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月11日首页
昨天以前首页

Vue SFC 编译器源码深度解析:processDefineEmits 与运行时事件生成机制

作者 excel
2025年11月8日 19:53

本文将深入剖析 Vue 单文件组件(SFC)编译器中的 defineEmits 处理逻辑,来自 compiler-sfc 模块的源码实现。
本文涵盖从概念、原理到实践的全链路分析,辅以详细注释与代码逐行解析。


一、概念:defineEmits 的定位与作用

在 Vue 3 的 <script setup> 中,defineEmits() 用于声明组件可以触发的事件。
例如:

const emit = defineEmits(['submit', 'cancel'])
emit('submit')

其核心目标是:

  • 类型层面:提供 TypeScript 类型约束(确保事件名称正确)。
  • 运行时层面:生成可用的 emits 配置,供 Vue 组件选项使用。

在编译阶段,Vue 会分析 defineEmits() 的调用,提取其中的事件定义或类型信息,并生成最终的运行时代码。


二、原理:编译器的处理流程总览

defineEmits 的编译处理分为三个核心步骤:

  1. 检测与记录调用processDefineEmits

    • 识别 defineEmits() 调用语句;
    • 检查重复定义;
    • 区分运行时参数和类型参数;
    • 保存到上下文(ScriptCompileContext)。
  2. 提取运行时声明extractRuntimeEmits

    • 若使用 TypeScript 类型参数(defineEmits<{...}>()),提取其中的事件名称。
  3. 生成最终运行时代码genRuntimeEmits

    • 合并类型定义与 v-model 的事件(如 update:modelValue);
    • 生成最终字符串代码片段。

三、源码逐行解析与注释

1. processDefineEmits

export function processDefineEmits(
  ctx: ScriptCompileContext,
  node: Node,
  declId?: LVal,
): boolean {
  if (!isCallOf(node, DEFINE_EMITS)) {
    return false
  }

说明
通过 isCallOf() 检查当前 AST 节点是否为 defineEmits() 的调用,若不是则跳过。


  if (ctx.hasDefineEmitCall) {
    ctx.error(`duplicate ${DEFINE_EMITS}() call`, node)
  }

说明
Vue 组件中只能调用一次 defineEmits,此处用于防止重复定义。


  ctx.hasDefineEmitCall = true
  ctx.emitsRuntimeDecl = node.arguments[0]

说明
记录运行时参数(如 ['click', 'submit'])。


  if (node.typeParameters) {
    if (ctx.emitsRuntimeDecl) {
      ctx.error(`${DEFINE_EMITS}() cannot accept both type and non-type arguments`, node)
    }
    ctx.emitsTypeDecl = node.typeParameters.params[0]
  }

说明
处理类型参数(如 defineEmits<{ change: (value: string) => void }>())。
同时检查不能混用类型参数与运行时参数。


  ctx.emitDecl = declId
  return true
}

说明
保存声明标识符(如 const emit = defineEmits(...)),供后续生成使用。


2. genRuntimeEmits

export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined {
  let emitsDecl = ''
  if (ctx.emitsRuntimeDecl) {
    emitsDecl = ctx.getString(ctx.emitsRuntimeDecl).trim()
  } else if (ctx.emitsTypeDecl) {
    const typeDeclaredEmits = extractRuntimeEmits(ctx)
    emitsDecl = typeDeclaredEmits.size
      ? `[${Array.from(typeDeclaredEmits)
          .map(k => JSON.stringify(k))
          .join(', ')}]`
      : ``
  }

说明
这里根据上下文选择不同模式:

  • 运行时参数:直接输出字符串。
  • 类型参数:调用 extractRuntimeEmits 从类型信息中提取事件名。

  if (ctx.hasDefineModelCall) {
    let modelEmitsDecl = `[${Object.keys(ctx.modelDecls)
      .map(n => JSON.stringify(`update:${n}`))
      .join(', ')}]`
    emitsDecl = emitsDecl
      ? `/*@__PURE__*/${ctx.helper('mergeModels')}(${emitsDecl}, ${modelEmitsDecl})`
      : modelEmitsDecl
  }
  return emitsDecl
}

说明
若组件同时使用 defineModel(),需合并 update:modelValue 事件。
生成形如:

/*@__PURE__*/_mergeModels(["submit"], ["update:modelValue"])

3. extractRuntimeEmits

export function extractRuntimeEmits(ctx: TypeResolveContext): Set<string> {
  const emits = new Set<string>()
  const node = ctx.emitsTypeDecl!

说明
当使用类型参数时,从类型 AST 中提取所有事件名称。


  if (node.type === 'TSFunctionType') {
    extractEventNames(ctx, node.parameters[0], emits)
    return emits
  }

说明
defineEmits 是函数类型(如 defineEmits<(e: 'click' | 'submit')>()),从参数中提取字符串字面量。


  const { props, calls } = resolveTypeElements(ctx, node)
  let hasProperty = false
  for (const key in props) {
    emits.add(key)
    hasProperty = true
  }

说明
如果是对象类型 { change: (val: number) => void },将键名作为事件名。


  if (calls) {
    if (hasProperty) {
      ctx.error(`defineEmits() type cannot mixed call signature and property syntax.`, node)
    }
    for (const call of calls) {
      extractEventNames(ctx, call.parameters[0], emits)
    }
  }
  return emits
}

说明
若类型中混合了函数签名与对象属性,会报错。
Vue 要求两者互斥,以保证一致性。


4. extractEventNames

function extractEventNames(
  ctx: TypeResolveContext,
  eventName: ArrayPattern | Identifier | ObjectPattern | RestElement,
  emits: Set<string>,
) {
  if (
    eventName.type === 'Identifier' &&
    eventName.typeAnnotation &&
    eventName.typeAnnotation.type === 'TSTypeAnnotation'
  ) {
    const types = resolveUnionType(ctx, eventName.typeAnnotation.typeAnnotation)

说明
此函数用于处理联合类型,如:

defineEmits<(e: 'a' | 'b' | 'c')>()

    for (const type of types) {
      if (type.type === 'TSLiteralType') {
        if (
          type.literal.type !== 'UnaryExpression' &&
          type.literal.type !== 'TemplateLiteral'
        ) {
          emits.add(String(type.literal.value))
        }
      }
    }
  }
}

说明
从联合类型中提取所有字面量值,并存入事件集合。


四、对比分析:类型模式 vs 运行时模式

模式类型 示例 优点 缺点
运行时参数 defineEmits(['click', 'change']) 简单直观 无类型约束
类型参数 defineEmits<{ change: (v: number) => void }>() 完整类型推导 仅 TS 支持
函数签名参数 `defineEmits<(e: 'a' 'b')>()` 可枚举事件字面量

Vue 的编译器在生成代码时,会自动兼容这些模式。


五、实践示例:从源码到生成代码

输入源代码:

const emit = defineEmits<{ submit: (data: any) => void, cancel: () => void }>()

编译器生成的运行时代码:

export default {
  emits: ["submit", "cancel"]
}

如果存在 defineModel('value')

export default {
  emits: /*@__PURE__*/_mergeModels(["submit", "cancel"], ["update:value"])
}

六、拓展:与 definePropsdefineModel 的关系

  • defineProps:声明组件输入接口;
  • defineEmits:声明组件输出事件;
  • defineModel:语法糖,自动生成 props + emits 的绑定关系。

genRuntimeEmits 之所以会检测 hasDefineModelCall,正是为了保证双向绑定事件正确注入。


七、潜在问题与限制

  1. 混用类型与运行时参数报错
    编译时强制互斥,否则抛出错误。

  2. 函数签名与对象类型混合报错
    例如:

    defineEmits<{
      submit: () => void
      (e: 'cancel'): void
    }>()
    

    这种混用是不被允许的。

  3. 无法动态计算事件名
    所有事件名称必须为字面量常量,否则类型提取失败。


八、总结

processDefineEmits 是 Vue SFC 编译过程中负责 事件定义提取 的关键函数。
它结合类型系统与运行时声明,生成统一的 emits 数组,保证组件在类型安全与运行时行为之间的平衡。

核心价值:

  • 静态分析事件声明;
  • 支持 TypeScript 类型提取;
  • 自动合并 v-model 相关事件。

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

🚀 从 GPT-5 流式输出看现代前端的流式请求机制(Koa 实现版)

作者 excel
2025年11月9日 08:44

一、前言:为什么要“流式输出”?

传统 HTTP 请求是「一次性返回完整结果」,而大模型(如 GPT-5)生成内容的过程往往比较慢。
如果要让用户看到“边生成边显示”的效果(像 ChatGPT 打字机一样),
就必须使用 流式响应(Streaming Response)


二、流式响应不是新协议

很多人会以为流式请求是某种新的 HTTP 方法,其实不是。

✅ 流式是 响应体的行为,不是请求方式的变化。
服务端仍然用普通的 POST,只是不会一次性 res.end(),而是持续往里写数据。

因此 GPT-5 的流式接口看起来是这样的:

POST /v1/chat/completions
Content-Type: application/json
Accept: text/event-stream

服务器边生成边 write(),客户端边读边显示。


三、为什么必须用 POST?

因为 GPT-5 请求通常包含复杂 JSON:

{
  "model": "gpt-5",
  "messages": [
    { "role": "user", "content": "解释量子纠缠" }
  ],
  "temperature": 0.7,
  "stream": true
}

只有 POST 请求 才能合法地携带请求体(body)。
GET 请求虽然也能带 query 参数,但长度有限、结构不适合复杂 JSON。

🧠 所以:“流式”是响应的特征,而“POST”是为了传递参数。


四、Koa 实现:服务端流式输出示例

下面是最小可运行的 Koa 流式响应示例

import Koa from "koa";
import Router from "@koa/router";
import bodyParser from "koa-bodyparser";

const app = new Koa();
const router = new Router();

router.post("/stream", async (ctx) => {
  const { prompt } = ctx.request.body;

  ctx.set("Content-Type", "text/event-stream");
  ctx.set("Cache-Control", "no-cache");
  ctx.set("Connection", "keep-alive");

  // 模拟 GPT-5 边生成边输出
  const text = `你发送的提示词是:${prompt}。\n下面是流式输出示例:`;
  for (const ch of text) {
    ctx.res.write(`data: ${ch}\n\n`);
    await new Promise((r) => setTimeout(r, 50)); // 模拟生成延迟
  }

  ctx.res.write("data: [DONE]\n\n");
  ctx.res.end();
});

app.use(bodyParser());
app.use(router.routes());
app.listen(3000, () => console.log("🚀 Server on http://localhost:3000/stream"));

🧠 说明:

  1. ctx.set("Content-Type", "text/event-stream") 告诉浏览器使用 SSE 流式响应
  2. 每个 ctx.res.write() 会立即发送一部分内容;
  3. 客户端不需要多次请求,而是持续读取同一个响应流。

五、前端如何接收流

前端用原生 fetch() 即可实现流式读取:

const res = await fetch("http://localhost:3000/stream", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ prompt: "你好,GPT-5!" }),
});

const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const chunk = decoder.decode(value);
  console.log("部分响应:", chunk);
}

控制台会实时打印字符流。
在真实应用中,你可以把这些内容插入编辑器或页面。


六、EventSource 可以替代吗?

不行(至少不完全行)。
EventSource 是浏览器内置的 SSE 客户端,但它:

  • ❌ 只支持 GET 请求;
  • ❌ 不能带请求体;
  • ✅ 适合广播、通知,而不是 GPT 请求。

所以如果你只要订阅服务器事件(例如“系统状态更新”),可以用 EventSource
但要给模型发 prompt、传 JSON,就必须用 fetch + ReadableStream


七、能不能用 ?query 传?

可以,但不推荐。
例如:

const source = new EventSource(`/stream?prompt=${encodeURIComponent("你好")}`);

这种方式仅适合短文本、教学演示。
如果 prompt 很长,或者要传多条 messages,URL 会溢出或被记录到日志中。
所以 GPT-5 这类接口建议还是:

POST 携带 JSON body
✅ 服务端用 text/event-stream 返回流式内容


八、对比三种实时通信方式

技术 请求体 响应方式 实时性 双向通信 适用场景
fetch + ReadableStream ✅ 支持 SSE / chunk ✅ 高 ❌ 单向 GPT-5 / AI 输出
EventSource ❌ 不支持 SSE ✅ 高 ❌ 单向 系统日志、状态广播
WebSocket ✅ 支持 Binary/Text ✅ 最高 ✅ 双向 聊天、协作、游戏

💡 GPT-5 这类接口的最佳实践:
POST + fetch + ReadableStream


九、结语

流式请求的核心并不在于“请求方式”,
而在于响应体的 分块(chunked transfer) 与浏览器对流的解析能力。

现代浏览器对流的支持越来越好,
这让我们能在前端直接用标准 API 实现 ChatGPT 那样的实时输出体验。

所以——
流式输出依然是 POST 请求,
只是响应体变成了连续的数据流。


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

深入解析 Vue 3 SSR 编译管线:ssrCodegenTransform 源码全解

作者 excel
2025年11月9日 08:18

一、背景与概念

在 Vue 3 的服务端渲染(SSR)体系中,模板编译器的职责是将 .vue 模板转化为服务端可执行的渲染函数代码。与客户端渲染(CSR)不同,SSR 输出的不是虚拟 DOM,而是完整 HTML 字符串,因此编译产物的结构和生成逻辑完全不同。

这篇文章将分析 Vue 源码中 ssrCodegenTransform.ts 文件的实现,它是SSR 编译阶段的核心转换器,主要负责:

  • 将模板 AST 转换成 JS AST;
  • 插入 SSR 特定的 helper 调用;
  • 组织字符串拼接逻辑;
  • 管理作用域与 CSS 变量。

二、核心函数:ssrCodegenTransform

📜 源码片段

export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions): void {
  const context = createSSRTransformContext(ast, options)

  if (options.ssrCssVars) {
    const cssContext = createTransformContext(createRoot([]), options)
    const varsExp = processExpression(
      createSimpleExpression(options.ssrCssVars, false),
      cssContext,
    )
    context.body.push(
      createCompoundExpression([`const _cssVars = { style: `, varsExp, `}`]),
    )
    Array.from(cssContext.helpers.keys()).forEach(helper => {
      ast.helpers.add(helper)
    })
  }

  const isFragment =
    ast.children.length > 1 && ast.children.some(c => !isText(c))
  processChildren(ast, context, isFragment)
  ast.codegenNode = createBlockStatement(context.body)

  ast.ssrHelpers = Array.from(
    new Set([
      ...Array.from(ast.helpers).filter(h => h in ssrHelpers),
      ...context.helpers,
    ]),
  )
  ast.helpers = new Set(Array.from(ast.helpers).filter(h => !(h in ssrHelpers)))
}

🧩 逐行解析

  1. 创建上下文:

    const context = createSSRTransformContext(ast, options)
    

    创建一个 SSR 专用的转换上下文,用于存储编译状态(包括 helper、body、错误回调等)。

  2. 注入 CSS 变量:

    if (options.ssrCssVars) { ... }
    

    当模板中使用了 SFC <style> 中定义的 CSS 变量时,需要生成 _cssVars 常量,确保 SSR 渲染时能正确解析。

  3. 判断是否为 Fragment:

    const isFragment = ast.children.length > 1 && ast.children.some(c => !isText(c))
    

    若根节点包含多个子节点(或非纯文本节点),需将其包裹为 <!--[--><!--]--> 片段标记。

  4. 核心递归处理:

    processChildren(ast, context, isFragment)
    

    将 AST 中的所有子节点转换为 JS 表达式或字符串片段。

  5. 生成最终代码块:

    ast.codegenNode = createBlockStatement(context.body)
    

    以 BlockStatement(JS 语法树节点)形式输出整个 SSR 渲染函数体。

  6. 区分 SSR 与 Vue 内置 helper:

    ast.ssrHelpers = ...
    

    将属于 SSR 渲染器的 helper(如 _push, _interpolate)从普通 Vue helper 中分离,供 @vue/server-renderer 使用。


三、上下文系统:createSSRTransformContext

📜 源码片段

function createSSRTransformContext(
  root: RootNode,
  options: CompilerOptions,
  helpers: Set<symbol> = new Set(),
  withSlotScopeId = false,
): SSRTransformContext {
  const body: BlockStatement['body'] = []
  let currentString: TemplateLiteral | null = null

  return {
    root,
    options,
    body,
    helpers,
    withSlotScopeId,
    onError: options.onError || (e => { throw e }),
    helper<T extends symbol>(name: T): T {
      helpers.add(name)
      return name
    },
    pushStringPart(part) {
      if (!currentString) {
        const currentCall = createCallExpression(`_push`)
        body.push(currentCall)
        currentString = createTemplateLiteral([])
        currentCall.arguments.push(currentString)
      }
      const bufferedElements = currentString.elements
      const lastItem = bufferedElements[bufferedElements.length - 1]
      if (isString(part) && isString(lastItem)) {
        bufferedElements[bufferedElements.length - 1] += part
      } else {
        bufferedElements.push(part)
      }
    },
    pushStatement(statement) {
      currentString = null
      body.push(statement)
    },
  }
}

🧠 原理讲解

SSRTransformContext 是整个编译流程的“状态容器”。
它的职责相当于一个“编译游标”:

  • 跟踪生成的语句(body);
  • 合并字符串模板;
  • 注册需要导入的 helper;
  • 捕获编译错误。

⚙️ 字符串缓冲机制

SSR 渲染主要是拼接字符串,因此引入了 pushStringPart() 方法,将所有连续的字符串合并在一个模板字面量(TemplateLiteral)中,减少 _push 调用开销。


四、核心节点遍历:processChildren

📜 源码片段

export function processChildren(
  parent: Container,
  context: SSRTransformContext,
  asFragment = false,
  disableNestedFragments = false,
  disableComment = false,
): void {
  if (asFragment) context.pushStringPart(`<!--[-->`)
  const { children } = parent
  for (let i = 0; i < children.length; i++) {
    const child = children[i]
    switch (child.type) {
      case NodeTypes.ELEMENT:
        switch (child.tagType) {
          case ElementTypes.ELEMENT:
            ssrProcessElement(child, context)
            break
          case ElementTypes.COMPONENT:
            ssrProcessComponent(child, context, parent)
            break
          case ElementTypes.SLOT:
            ssrProcessSlotOutlet(child, context)
            break
        }
        break
      case NodeTypes.TEXT:
        context.pushStringPart(escapeHtml(child.content))
        break
      case NodeTypes.INTERPOLATION:
        context.pushStringPart(
          createCallExpression(context.helper(SSR_INTERPOLATE), [child.content]),
        )
        break
      case NodeTypes.IF:
        ssrProcessIf(child, context, disableNestedFragments, disableComment)
        break
      case NodeTypes.FOR:
        ssrProcessFor(child, context, disableNestedFragments)
        break
      case NodeTypes.COMMENT:
        if (!disableComment) {
          context.pushStringPart(`<!--${child.content}-->`)
        }
        break
    }
  }
  if (asFragment) context.pushStringPart(`<!--]-->`)
}

🧩 核心逻辑分解

  1. Fragment 包裹:
    使用注释节点标记多节点片段,便于客户端 hydration 对齐。

  2. 节点类型分派:

    • 元素节点:委托给 ssrProcessElement()
    • 组件节点:调用 ssrProcessComponent()
    • 插槽节点:使用 ssrProcessSlotOutlet()
    • 文本节点:HTML 转义后直接拼接。
    • 插值表达式:调用 _interpolate helper。
    • 条件 / 循环节点:分别交由 ssrProcessIfssrProcessFor 处理。
  3. 注释节点:
    默认输出到最终 HTML 中,但可以通过 disableComment 抑制。


五、子上下文机制:createChildContext

function createChildContext(
  parent: SSRTransformContext,
  withSlotScopeId = parent.withSlotScopeId,
): SSRTransformContext {
  return createSSRTransformContext(
    parent.root,
    parent.options,
    parent.helpers,
    withSlotScopeId,
  )
}

每个子作用域(如插槽或循环体)都会派生一个新的 context,但共享相同的 helpers 集合。
这样既能独立管理作用域,又能复用 helper 注册,保证生成代码一致。


六、实践与拓展

✅ 实践意义

理解 ssrCodegenTransform 有助于:

  • 编写自定义 SSR 指令;
  • 调试模板编译输出;
  • 改进服务端渲染性能;
  • 探索 @vue/compiler-ssr 的扩展。

🧩 拓展方向

  • 字符串合并优化:可尝试 AST 层级的批量合并;
  • 流式 SSR:在 _push 调用处加入流写入逻辑;
  • 定制 helper 注入:通过 context.helper() 动态扩展 SSR runtime。

⚠️ 潜在问题

  • CSS 变量注入只在顶层执行,若嵌套作用域需手动传递;
  • 片段注释标记若被破坏,会导致客户端 hydration 错位;
  • 对于复杂组件(如异步组件、Teleport)还需额外的 transform 支持。

七、总结

ssrCodegenTransform 是 Vue 3 SSR 编译管线的关键环节,它将模板 AST 转化为服务端渲染可执行的 JS AST。其设计充分体现了 分层抽象 + 上下文状态隔离 + 字符串缓冲优化 的思想。

理解此模块能让开发者深入掌握 Vue SSR 的底层工作原理,也为二次开发和性能优化提供了坚实基础。


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

Vue SSR 运行时辅助工具注册机制源码详解

作者 excel
2025年11月9日 08:16

本文将深入解析 Vue 在服务端渲染(SSR)过程中对运行时辅助工具(Runtime Helpers)的注册机制。我们将从概念出发,结合源码剖析其设计原理、用途及扩展性。


一、概念层:SSR 与运行时辅助工具(Runtime Helpers)

在 Vue 的 服务端渲染 (Server-Side Rendering, SSR) 体系中,编译器会将模板转化为可执行的渲染函数。然而,为了简化代码生成和运行时逻辑复用,Vue 将常见的渲染任务抽象为一系列 运行时辅助函数(runtime helpers)

这些 helpers 负责处理:

  • 插值(ssrInterpolate
  • 元素渲染(ssrRenderVNode
  • 组件渲染(ssrRenderComponent
  • 样式与属性处理(ssrRenderStyle, ssrRenderAttrs
  • 动态模型、插槽、Teleport、Suspense 等复杂结构

这些函数被标识为 独立的 Symbol 常量,并在运行时注册到编译器中,以便生成模板代码时可以正确引用。


二、原理层:Symbol 注册与映射机制

核心源码如下:

import { registerRuntimeHelpers } from '@vue/compiler-dom'

export const SSR_INTERPOLATE: unique symbol = Symbol(`ssrInterpolate`)
export const SSR_RENDER_VNODE: unique symbol = Symbol(`ssrRenderVNode`)
export const SSR_RENDER_COMPONENT: unique symbol = Symbol(`ssrRenderComponent`)
...
export const ssrHelpers: Record<symbol, string> = {
  [SSR_INTERPOLATE]: `ssrInterpolate`,
  [SSR_RENDER_VNODE]: `ssrRenderVNode`,
  ...
}

// 注册所有 SSR helpers
registerRuntimeHelpers(ssrHelpers)

核心逻辑解析:

  1. Symbol 定义

    export const SSR_INTERPOLATE: unique symbol = Symbol(`ssrInterpolate`)
    
    • 每个 helper 使用 Symbol() 生成独一无二的标识,防止命名冲突。
    • TypeScript 的 unique symbol 类型确保类型系统能静态识别这些常量。
  2. 映射表(ssrHelpers)

    export const ssrHelpers: Record<symbol, string> = { ... }
    
    • 将 Symbol 映射到对应字符串名称。
    • 这些名称必须与 @vue/server-renderer 中的 helper 函数名严格一致,否则在 SSR 构建阶段会发生运行时错误。
  3. 运行时注册

    registerRuntimeHelpers(ssrHelpers)
    
    • 调用 @vue/compiler-dom 提供的注册函数,将所有 helpers 注册到编译器内部的 helper 表。
    • 编译模板时,如果模板使用了某个 SSR 功能(如 <Suspense> 或插槽),编译器会自动注入相应 helper 的引用。

三、对比层:SSR 与 CSR(客户端渲染)Helper 的区别

对比维度 客户端渲染 (CSR) Helpers 服务端渲染 (SSR) Helpers
执行环境 浏览器(DOM 操作) Node.js 或 Render Context
输出目标 虚拟 DOM (VNode) 字符串 HTML
注册方式 registerRuntimeHelpers (在编译器层注册) 同样机制,但 helper 名称不同
典型函数 createVNode, renderList ssrRenderComponent, ssrInterpolate
编译结果 JS runtime 生成 DOM 直接生成 HTML 字符串流

因此 SSR helpers 是一组“无副作用”的函数,专门用于在服务端渲染阶段拼接 HTML,而非操作浏览器 DOM。


四、实践层:自定义 SSR Helper 示例

你可以通过相同机制扩展 SSR helper,例如添加自定义格式化输出函数:

export const SSR_FORMAT_DATE: unique symbol = Symbol('ssrFormatDate')

const customHelpers = {
  [SSR_FORMAT_DATE]: 'ssrFormatDate'
}

registerRuntimeHelpers(customHelpers)

并在 @vue/server-renderer 中实现对应函数:

export function ssrFormatDate(value) {
  return new Date(value).toLocaleDateString()
}

使用效果:

<p>{{ ssrFormatDate(user.createdAt) }}</p>

在 SSR 输出阶段,该模板会调用注册的 ssrFormatDate() 方法直接生成字符串。


五、拓展层:SSR Helper 注册的作用链路

完整流程如下:

模板编译阶段 (compiler-dom)
     ↓
AST 转换阶段 (transform)
     ↓
检测所需 helpers
     ↓
调用 registerRuntimeHelpers 注册
     ↓
生成 render 函数时注入 import 语句
     ↓
@vue/server-renderer 提供对应函数实现
     ↓
最终 HTML 字符串输出

这种分层架构使得 Vue SSR 模块具备:

  • 解耦性强(编译器与运行时分离)
  • 可扩展性高(支持自定义 helper)
  • 类型安全性(TypeScript 的 unique symbol

六、潜在问题与注意事项

  1. 命名必须与运行时实现严格对应
    ssrHelpers 中的字符串与 @vue/server-renderer 实现不一致,会导致运行时抛出 undefined is not a function
  2. SSR 与 CSR Helper 不可混用
    SSR helper 仅在服务端使用,客户端 hydration 阶段应依赖客户端 helper。
  3. 不应在模板中直接引用未注册 helper
    模板编译器只识别已注册的 helper,否则编译器无法生成合法代码。

七、总结

本文展示了 Vue SSR 运行时辅助函数的注册机制:

  • 通过 unique symbol 保证唯一性;
  • 通过 registerRuntimeHelpers 注册至编译器;
  • 在编译阶段根据模板特性自动注入相应 helper;
  • 最终在服务端渲染时调用具体的字符串生成函数。

这种设计实现了 SSR 模块的模块化与灵活性,是 Vue 3 编译器与运行时架构分离思想的典型体现。


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

深入解析 Vue SSR 编译器的核心函数:compile

作者 excel
2025年11月9日 08:15

Vue 3 的服务端渲染(SSR, Server-Side Rendering)体系中,compile() 是一个关键函数。它负责将模板字符串或 AST 抽象语法树转化为 可在服务端执行的渲染函数,以生成最终的 HTML 字符串。

本文我们将深入解析 compile 的完整实现,剖析其底层机制与设计哲学。


一、概念:compile 是做什么的?

compile 是 Vue SSR 编译流程的入口函数,功能上类似于前端版本的 @vue/compiler-domcompile,但专为服务端渲染优化。
它主要完成以下几个任务:

  1. 解析模板(Parsing)→ 生成 AST;
  2. 执行转换(Transform)→ 为 SSR 注入特定逻辑;
  3. 生成代码(Codegen)→ 输出服务端可执行的渲染函数代码。

核心目标:把 .vue 模板转化为服务端可运行的渲染函数,使得同一模板在 Node.js 环境下可以生成 HTML 字符串。


二、原理:从模板到渲染函数的编译流程

来看完整源码:

export function compile(
  source: string | RootNode,
  options: CompilerOptions = {},
): CodegenResult {
  options = {
    ...options,
    ...parserOptions,
    ssr: true,
    inSSR: true,
    scopeId: options.mode === 'function' ? null : options.scopeId,
    prefixIdentifiers: true,
    cacheHandlers: false,
    hoistStatic: false,
  }

  const ast = typeof source === 'string' ? baseParse(source, options) : source
  rawOptionsMap.set(ast, options)

  transform(ast, {
    ...options,
    hoistStatic: false,
    nodeTransforms: [
      transformVBindShorthand,
      ssrTransformIf,
      ssrTransformFor,
      trackVForSlotScopes,
      transformExpression,
      ssrTransformSlotOutlet,
      ssrInjectFallthroughAttrs,
      ssrInjectCssVars,
      ssrTransformElement,
      ssrTransformComponent,
      trackSlotScopes,
      transformStyle,
      ...(options.nodeTransforms || []),
    ],
    directiveTransforms: {
      bind: transformBind,
      on: transformOn,
      model: ssrTransformModel,
      show: ssrTransformShow,
      cloak: noopDirectiveTransform,
      once: noopDirectiveTransform,
      memo: noopDirectiveTransform,
      ...(options.directiveTransforms || {}),
    },
  })

  ssrCodegenTransform(ast, options)

  return generate(ast, options)
}

🔍 步骤 1:配置编译选项

options = {
  ...options,
  ...parserOptions,
  ssr: true,
  inSSR: true,
  scopeId: options.mode === 'function' ? null : options.scopeId,
  prefixIdentifiers: true,
  cacheHandlers: false,
  hoistStatic: false,
}

说明:

  • ssr: trueinSSR: true → 明确告诉编译器处于服务端渲染模式;
  • prefixIdentifiers: true → 在 SSR 模式下启用变量前缀(如 _ctx.),避免作用域冲突;
  • cacheHandlershoistStatic 被禁用,因为 SSR 没有客户端 diff 的性能需求。

注释:

// SSR 模式下需要明确开启服务端标志
// 并关闭前端优化(如事件缓存、静态提升)

🔍 步骤 2:生成或使用已有 AST

const ast = typeof source === 'string' ? baseParse(source, options) : source
rawOptionsMap.set(ast, options)

说明:

  • 若传入字符串模板,则调用 baseParse() 将其解析为 AST;
  • 若已是 RootNode(抽象语法树),则直接使用;
  • rawOptionsMap.set() 保存编译配置,用于后续子树的 SSR 转换(尤其是 <slot>)。

🔍 步骤 3:执行 AST 转换(Transform 阶段)

transform(ast, {
  ...options,
  nodeTransforms: [...],
  directiveTransforms: {...},
})

关键:

这一阶段将模板 AST 转化为 SSR 友好的中间表示(IR)

核心 Node Transforms:

转换函数 作用
transformVBindShorthand 处理 :prop 的简写绑定
ssrTransformIf v-if 转化为条件渲染表达式
ssrTransformFor v-for 转化为循环渲染
trackVForSlotScopes 跟踪 v-for 中的插槽作用域
ssrTransformSlotOutlet 改写 <slot> 为 SSR 输出函数
ssrInjectFallthroughAttrs 处理组件透传属性
ssrInjectCssVars 注入 SSR 版本的 CSS 变量
ssrTransformElement 核心:将普通元素节点转化为 SSR 可渲染字符串
ssrTransformComponent 组件级别的 SSR 转换
transformStyle 处理样式绑定(v-bind:style

指令转换(Directive Transforms):

指令 转换函数 说明
v-bind transformBind 保留 DOM 编译逻辑
v-on transformOn 保留事件逻辑(部分忽略)
v-model ssrTransformModel SSR 特殊处理双向绑定
v-show ssrTransformShow 转化为服务端条件渲染
v-cloak/once/memo noopDirectiveTransform 在 SSR 阶段被忽略

🔍 步骤 4:SSR 专用代码生成阶段

ssrCodegenTransform(ast, options)

这一阶段会扫描并修改 ast.codegenNode,将其替换为 SSR 代码生成树

这一步是 SSR 的“魔法”所在,它将模板结构转化为字符串拼接逻辑,例如:

<div>{{ msg }}</div>

会被编译成:

push(`<div>${_ctx.msg}</div>`)

🔍 步骤 5:生成最终渲染函数

return generate(ast, options)

最终输出的 CodegenResult 包含:

  • 渲染函数字符串;
  • 依赖导入信息;
  • SSR 上下文管理代码。

三、对比:SSR 编译 vs. DOM 编译

特性 DOM 编译(客户端) SSR 编译(服务端)
输出 渲染函数(VNode 树) 渲染函数(HTML 字符串)
优化 静态提升、事件缓存 字符串拼接优化
指令处理 运行时 patch 编译期生成逻辑
样式作用域 动态添加 编译时注入
运行环境 浏览器 Node.js

可以看出 SSR 编译器去掉了许多“前端运行时优化”,换取 编译期确定性执行速度


四、实践:如何使用 compile

以下是一个最小示例:

import { compile } from '@vue/compiler-ssr'

const result = compile(`<div>Hello {{ name }}</div>`)
console.log(result.code)

输出示例(简化):

function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div>Hello ${_ctx.name}</div>`)
}

这段代码可直接在 Node 环境中执行,用于服务端输出 HTML。


五、拓展:SSR 的子编译流程

SSR 编译器还支持:

  • 插槽内容(slot branches)进行独立编译;
  • 支持 CSS 变量注入;
  • 支持自定义 directiveTransforms,用于扩展 SSR 指令。

开发者可通过 options.nodeTransformsoptions.directiveTransforms 注入自定义逻辑,实现个性化的 SSR 编译管线。


六、潜在问题与注意事项

  1. 与 hydration 不兼容的行为
    某些指令如 v-oncev-memo 无法在 SSR 端使用,会在 hydration 时失效。
  2. CSS 变量同步问题
    ssrInjectCssVars 仅编译注入变量,但客户端需同步以避免闪烁。
  3. 性能陷阱
    若模板过大,generate() 阶段可能生成极长字符串;可考虑分块渲染。

总结

compile() 是 Vue SSR 编译器的核心接口,它将模板编译为可执行的字符串生成函数,是从模板到 HTML 的桥梁。
通过多层 transform 管线与 SSR 专用 codegen,Vue 实现了优雅的模板到字符串编译机制。


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

Vue SSR 错误系统源码解析:createSSRCompilerError 与 SSRErrorCodes 的设计原理

作者 excel
2025年11月9日 08:13

在 Vue 3 的服务端渲染(Server-Side Rendering, SSR)编译器实现中,错误系统是一个极其关键的组成部分。本文将深入剖析 createSSRCompilerErrorSSRErrorCodes 以及相关机制,展示 Vue SSR 编译阶段如何优雅地捕获、标识并提示错误。


一、概念层:SSR 编译错误系统概述

在 Vue 的编译体系中,错误处理的目标是让开发者清晰地知道编译器在哪一步、为何失败。SSR 编译器与普通 DOM 编译器相比,有一些特殊的逻辑和语义,因此需要额外定义 SSR 专属错误码(SSRErrorCodes错误创建函数(createSSRCompilerError

简而言之,createSSRCompilerError 是 SSR 版的 createCompilerError,两者共享底层逻辑,但拥有独立的错误编号与错误信息表。


二、原理层:源码逐行拆解与设计逻辑

核心导入部分

import {
  type CompilerError,
  DOMErrorCodes,
  type SourceLocation,
  createCompilerError,
} from '@vue/compiler-dom'
  • CompilerError:Vue 编译器通用的错误类型定义。
  • DOMErrorCodes:DOM 编译器的错误编号集合。
  • createCompilerError:标准错误构造函数,用于生成带位置信息的错误对象。
  • SourceLocation:用于标注源码中错误位置的结构体(包括行、列、偏移量等)。

这些导入为 SSR 错误模块提供了类型约束与基类能力。


定义 SSR 特化错误类型

export interface SSRCompilerError extends CompilerError {
  code: SSRErrorCodes
}

这里通过 接口扩展(interface extends)CompilerError 泛化为 SSRCompilerError
区别在于:code 字段不再引用 DOMErrorCodes,而是使用 SSR 自己定义的 SSRErrorCodes

✅ 目的:防止 SSR 与 DOM 错误码空间冲突,同时让 TypeScript 能识别不同来源的错误类型。


创建 SSR 编译错误函数

export function createSSRCompilerError(
  code: SSRErrorCodes,
  loc?: SourceLocation,
) {
  return createCompilerError(code, loc, SSRErrorMessages) as SSRCompilerError
}

这一函数是整个模块的核心逻辑。

  • 参数 code:指定错误编号。
  • 参数 loc:错误在源文件中的位置。
  • 第三个参数 SSRErrorMessages:错误码到错误消息的映射表。

最终返回值是通过类型断言强制转换为 SSRCompilerError 的对象。

🔍 底层机制说明:
createCompilerError 会创建形如 { code, loc, message } 的错误对象。SSR 版本只是传入自己的 message 映射表,因此整个系统能自动输出 SSR 特有的提示。


定义 SSR 错误枚举

export enum SSRErrorCodes {
  X_SSR_UNSAFE_ATTR_NAME = 65 /* DOMErrorCodes.__EXTEND_POINT__ */,
  X_SSR_NO_TELEPORT_TARGET,
  X_SSR_INVALID_AST_NODE,
}
  • X_SSR_UNSAFE_ATTR_NAME = 65:以 DOM 错误系统的扩展点为基准。
  • 后续枚举项自增:6667
  • 注释中的 __EXTEND_POINT__ 是一个 同步锚点,用于保证不同编译模块之间错误码不重叠。

⚙️ 机制解读:
DOMErrorCodes.__EXTEND_POINT__ 是 DOM 模块预留的扩展区起点。
SSR 模块在此之后定义自己的错误码,从而保证系统内的唯一性。


测试保护逻辑

if (__TEST__) {
  if (SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME < DOMErrorCodes.__EXTEND_POINT__) {
    throw new Error(
      `SSRErrorCodes need to be updated to ${
        DOMErrorCodes.__EXTEND_POINT__
      } to match extension point from core DOMErrorCodes.`,
    )
  }
}

该块代码只在单元测试环境中执行,用于防止 SSR 错误码与 DOM 错误码冲突

🧠 思路解读:
由于 enum 自增逻辑依赖前值,而不同模块可能被拆分编译,因此测试阶段通过此断言确保 SSR 错误编号始终 ≥ DOM 错误扩展点。


定义错误消息映射表

export const SSRErrorMessages: { [code: number]: string } = {
  [SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME]: `Unsafe attribute name for SSR.`,
  [SSRErrorCodes.X_SSR_NO_TELEPORT_TARGET]: `Missing the 'to' prop on teleport element.`,
  [SSRErrorCodes.X_SSR_INVALID_AST_NODE]: `Invalid AST node during SSR transform.`,
}

这部分是错误码到错误消息的映射表:

错误码 错误信息 含义
X_SSR_UNSAFE_ATTR_NAME Unsafe attribute name for SSR. 属性名在 SSR 环境中不安全(如事件绑定)。
X_SSR_NO_TELEPORT_TARGET Missing the 'to' prop on teleport element. teleport 组件缺少 to 属性。
X_SSR_INVALID_AST_NODE Invalid AST node during SSR transform. 在 SSR 转换阶段检测到无效的 AST 节点。

这类表驱动映射能让编译器在运行时快速定位错误,而无需条件分支判断。


三、对比层:SSR 与 DOM 错误系统的差异

对比维度 DOM 错误系统 SSR 错误系统
错误枚举 DOMErrorCodes SSRErrorCodes
消息表 DOMErrorMessages SSRErrorMessages
扩展策略 内部定义 + 保留扩展点 从 DOM 扩展点继续定义
主要用途 客户端模板编译 服务器端渲染转换
检查机制 DOM 专属 AST 校验 SSR AST 与运行时安全性

四、实践层:错误捕获与调试示例

try {
  throw createSSRCompilerError(
    SSRErrorCodes.X_SSR_NO_TELEPORT_TARGET,
    { start: 12, end: 24 } as any
  )
} catch (err) {
  console.error(err.message)
}

输出:

Missing the 'to' prop on teleport element.

分步解释:

  1. 创建错误对象:调用 createSSRCompilerError 并传入错误码。
  2. 附加位置信息:在 loc 中指定源码范围。
  3. 错误输出:内部根据 SSRErrorMessages 查表生成友好的消息文本。

五、拓展层:Vue 编译器的错误生态体系

SSR 错误系统只是 Vue 编译器错误体系的一环,其他模块包括:

  • DOMErrorCodes:处理模板语法与指令问题。
  • CompilerError:统一错误接口。
  • transform 与 parser:通过上下文(context)传播错误对象。
  • onError 回调机制:允许上层(如 Vue Loader)捕获错误并友好提示。

通过这种模块化设计,Vue 编译器能在不同阶段复用通用错误逻辑,并保持类型安全。


六、潜在问题与改进思路

  1. 枚举值手动同步风险
    若 DOM 扩展点更新但 SSR 未调整,将导致测试失败。可考虑使用自动脚本同步。
  2. 错误码分布不连续
    若未来引入更多 SSR 模块,可能需要重新规划错误码段。
  3. 国际化支持不足
    当前错误信息仅英文,未来可引入多语言映射表。

总结

createSSRCompilerError 模块体现了 Vue 编译器体系中严谨的模块分层与扩展策略。
通过枚举、映射表与类型系统的协作,Vue 能在 SSR 编译阶段精准定位错误,保证开发者在调试复杂渲染逻辑时得到清晰反馈。


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

Vue SSR 编译器源码深析:ssrTransformShow 的实现原理与设计哲学

作者 excel
2025年11月9日 08:11

一、概念理解:v-show 在 SSR 环境中的本质问题

在 Vue 的客户端渲染(CSR)中,v-show 是通过动态修改元素的 display 样式属性来控制显隐的。
但在 SSR(Server-Side Rendering) 环境下,没有真实的 DOM 操作,因此必须在 编译阶段 将其转换为合适的“样式表达式”,以便在渲染 HTML 时即决定元素的显示与否。

这段源码正是 Vue SSR 编译阶段对 v-show 指令的专用转换函数:ssrTransformShow


二、原理剖析:从 AST 转换到可执行的 SSR 表达式

源码如下:

import {
  DOMErrorCodes,
  type DirectiveTransform,
  createConditionalExpression,
  createDOMCompilerError,
  createObjectExpression,
  createObjectProperty,
  createSimpleExpression,
} from '@vue/compiler-dom'

export const ssrTransformShow: DirectiveTransform = (dir, node, context) => {
  if (!dir.exp) {
    context.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION),
    )
  }
  return {
    props: [
      createObjectProperty(
        `style`,
        createConditionalExpression(
          dir.exp!,
          createSimpleExpression(`null`, false),
          createObjectExpression([
            createObjectProperty(
              `display`,
              createSimpleExpression(`none`, true),
            ),
          ]),
          false /* no newline */,
        ),
      ),
    ],
  }
}

🔍 逐行注释讲解

1. 导入编译器工具函数

import {
  DOMErrorCodes,                // DOM 编译器错误码枚举
  type DirectiveTransform,       // 指令转换器类型定义
  createConditionalExpression,   // 创建条件(三元)表达式节点
  createDOMCompilerError,        // 创建编译错误对象
  createObjectExpression,        // 创建对象字面量表达式节点
  createObjectProperty,          // 创建对象属性节点
  createSimpleExpression,        // 创建简单表达式节点
} from '@vue/compiler-dom'

👉 这些函数用于生成 Vue 编译器 AST(抽象语法树)节点,使模板语法转换为 JavaScript 渲染函数表达式。


2. 定义 ssrTransformShow

export const ssrTransformShow: DirectiveTransform = (dir, node, context) => {
  • dir:指令对象,包含 nameexpmodifiers 等信息。
  • node:当前处理的 AST 节点(如 <div v-show="visible">)。
  • context:编译上下文,包含错误处理、代码生成状态等。

3. 错误处理:未传入表达式时报错

if (!dir.exp) {
  context.onError(
    createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION),
  )
}

🧩 解释:

  • v-show 未绑定表达式(如 <div v-show>),在编译阶段抛出 X_V_SHOW_NO_EXPRESSION 错误。
  • 这是语义层检查,确保 SSR 输出逻辑正确。

4. 返回转换结果:生成 SSR 样式表达式

return {
  props: [
    createObjectProperty(
      `style`,
      createConditionalExpression(
        dir.exp!,                      // 条件:若表达式为真
        createSimpleExpression(`null`, false),   // 则样式为 null(保持默认)
        createObjectExpression([       // 否则强制设置 display:none
          createObjectProperty(
            `display`,
            createSimpleExpression(`none`, true),
          ),
        ]),
        false /* no newline */,
      ),
    ),
  ],
}

🧠 逻辑转换效果如下:

模板:

<div v-show="visible"></div>

编译成 SSR 渲染表达式:

{
  style: visible ? null : { display: "none" }
}

此结构在服务端渲染时即能决定元素是否隐藏,而不依赖浏览器执行逻辑。


三、对比分析:v-show vs v-if 在 SSR 中的差异

指令 渲染机制 SSR 表现 性能特征
v-if 条件性渲染(创建/销毁 DOM) 服务端直接生成或省略对应 HTML 轻量但有重绘代价
v-show 样式控制(display: none 服务端生成元素但隐藏 保持结构完整,适用于频繁切换显示

📌 结论:ssrTransformShow 的存在,使得 v-show 也能在 SSR 输出阶段保留 DOM 结构一致性,有利于 hydration(客户端激活) 一致性。


四、实践部分:自定义 SSR 指令转换示例

假设我们要实现一个类似 v-visible 的 SSR 指令,其逻辑与 v-show 类似但控制 visibility

export const ssrTransformVisible: DirectiveTransform = (dir, node, context) => {
  if (!dir.exp) {
    context.onError(createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION))
  }
  return {
    props: [
      createObjectProperty(
        `style`,
        createConditionalExpression(
          dir.exp!,
          createSimpleExpression(`null`, false),
          createObjectExpression([
            createObjectProperty(
              `visibility`,
              createSimpleExpression(`hidden`, true),
            ),
          ]),
          false,
        ),
      ),
    ],
  }
}

🔧 编译结果:

<div v-visible="isVisible"></div>

➡ SSR 渲染后:

{
  style: isVisible ? null : { visibility: "hidden" }
}

五、拓展思考:SSR 编译器的可扩展性机制

Vue 编译器的设计高度模块化。
所有指令都通过类似的 DirectiveTransform 接口实现:

interface DirectiveTransform {
  (dir: DirectiveNode, node: ElementNode, context: TransformContext): TransformResult
}

因此开发者可自由扩展:

  • 新增自定义指令;
  • 为 SSR 定义独立转换逻辑;
  • 拓展 AST 节点结构;
  • 实现插件式编译中间层。

这也是 Vue 编译器能支持多平台(Web、SSR、Custom Renderer)的关键机制。


六、潜在问题与注意事项

  1. v-show 的动态性缺失
    SSR 仅处理初始渲染,后续仍需在客户端由 runtime 更新样式。
    若表达式依赖异步数据,则 SSR 阶段无法精确控制。
  2. 样式覆盖冲突
    若模板或 CSS 已设置 display 样式,SSR 输出的内联样式可能被覆盖。
  3. hydration 不一致风险
    若 SSR 阶段与客户端初始数据不一致,会造成 hydration mismatch。

总结

ssrTransformShow 是 Vue SSR 编译管线中的一个小而精的组件,其核心使命是:

将“显示控制指令”转译为“样式表达式”,确保 SSR 输出结构完整且可预测。

通过 createConditionalExpressioncreateObjectExpression 等函数的组合,Vue 实现了“模板 → AST → SSR 表达式”的全链条自动化编译。


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

Vue SSR 源码解析:ssrTransformModel 深度剖析

作者 excel
2025年11月9日 08:09

一、概念

在 Vue 的 SSR(服务端渲染)编译阶段中,v-model 指令的处理逻辑与客户端渲染存在显著差异。
客户端的 v-model 依赖运行时双向绑定机制,而 SSR 需要在编译时就生成静态字符串输出,因此必须提供一个对应的 SSR 版本 transformssrTransformModel

它的任务是:

  • v-model 指令在服务端编译阶段转化为适用于服务端的渲染表达式;
  • 自动为不同类型的表单控件(inputtextareaselect 等)注入正确的 SSR 绑定逻辑;
  • 确保在模板被渲染成字符串时,v-model 的值反映在 DOM 属性中(例如选中状态或输入值)。

二、原理

ssrTransformModel 是一个 DirectiveTransform,即用于指令编译的转换函数。它通过匹配 v-model 指令节点,根据节点类型(HTML 元素类型)生成对应的 SSR 代码片段。

核心思路如下:

  1. 判断节点类型

    • 普通元素(inputtextareaselect) → 服务端静态输出;
    • 组件节点(<MyInput v-model="x" />) → 委托给 transformModel()
  2. 针对不同元素的处理逻辑

    • input[type=text]:输出 value 属性;
    • input[type=radio]:根据 v-model 值是否匹配当前选项生成 checked
    • input[type=checkbox]:根据数组/布尔判断生成 checked
    • textarea:将内部内容替换为插值;
    • select:递归处理所有 option,为选中项添加 selected 属性。
  3. 错误与校验

    • 检查不合法用法,如:

      • v-modelvalue 同时存在;
      • v-model 用于 <input type="file">
      • v-model 用于非表单元素。

三、源码与逐行注释

1. 引入与类型声明

import {
  DOMErrorCodes,
  type DirectiveTransform,
  ElementTypes,
  type ExpressionNode,
  NodeTypes,
  type PlainElementNode,
  type TemplateChildNode,
  createCallExpression,
  createConditionalExpression,
  createDOMCompilerError,
  createInterpolation,
  createObjectProperty,
  createSimpleExpression,
  findProp,
  hasDynamicKeyVBind,
  transformModel,
} from '@vue/compiler-dom'
import {
  SSR_INCLUDE_BOOLEAN_ATTR,
  SSR_LOOSE_CONTAIN,
  SSR_LOOSE_EQUAL,
  SSR_RENDER_DYNAMIC_MODEL,
} from '../runtimeHelpers'
import type { DirectiveTransformResult } from 'packages/compiler-core/src/transform'

说明:

  • 前半部分导入 AST 处理相关工具函数(如 createCallExpressioncreateConditionalExpression 等)。
  • 后半部分导入 SSR 渲染辅助函数(如 SSR_LOOSE_EQUALSSR_RENDER_DYNAMIC_MODEL 等),这些是 SSR 运行时在字符串拼接中使用的 helper。

2. 核心 transform 函数定义

export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
  const model = dir.exp!

说明:

  • dir:表示 v-model 指令节点;
  • node:当前绑定的元素节点;
  • context:编译上下文,用于错误报告、helper 注册等;
  • modelv-model 的表达式节点(如 foo.bar)。

3. 检查重复绑定 value

  function checkDuplicatedValue() {
    const value = findProp(node, 'value')
    if (value) {
      context.onError(
        createDOMCompilerError(
          DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
          value.loc,
        ),
      )
    }
  }

逻辑说明:
SSR 不需要再显式设置 value,因为 v-model 已自动生成它;若模板中多写一个 value,会报错提示用户。


4. 递归处理 <select> 子节点

  const processSelectChildren = (children: TemplateChildNode[]) => {
    children.forEach(child => {
      if (child.type === NodeTypes.ELEMENT) {
        processOption(child as PlainElementNode)
      } else if (child.type === NodeTypes.FOR) {
        processSelectChildren(child.children)
      } else if (child.type === NodeTypes.IF) {
        child.branches.forEach(b => processSelectChildren(b.children))
      }
    })
  }

逻辑说明:
递归遍历 <select> 的所有层级子节点,包括被 v-forv-if 包裹的选项,确保每个 <option> 都能正确标注 selected 状态。


5. 处理 <option> 元素

  function processOption(plainNode: PlainElementNode) {
    if (plainNode.tag === 'option') {
      if (plainNode.props.findIndex(p => p.name === 'selected') === -1) {
        const value = findValueBinding(plainNode)
        plainNode.ssrCodegenNode!.elements.push(
          createConditionalExpression(
            createCallExpression(context.helper(SSR_INCLUDE_BOOLEAN_ATTR), [              createConditionalExpression(                createCallExpression(`Array.isArray`, [model]),
                createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [                  model,                  value,                ]),
                createCallExpression(context.helper(SSR_LOOSE_EQUAL), [                  model,                  value,                ]),
              ),
            ]),
            createSimpleExpression(' selected', true),
            createSimpleExpression('', true),
            false,
          ),
        )
      }
    } else if (plainNode.tag === 'optgroup') {
      processSelectChildren(plainNode.children)
    }
  }

逐步说明:

  1. <option> 没有显式 selected,则判断它是否应被选中;

  2. 判断逻辑:

    • 如果 v-model 是数组,则调用 SSR_LOOSE_CONTAIN(model, value)
    • 否则用 SSR_LOOSE_EQUAL(model, value)
  3. 若判断为真,则拼接字符串 " selected" 到最终 SSR 输出。


6. 主逻辑分支(根据元素类型)

  if (node.tagType === ElementTypes.ELEMENT) {
    const res: DirectiveTransformResult = { props: [] }
    const defaultProps = [
      createObjectProperty(`value`, model),
    ]

创建 transform 结果对象 res,用于存放编译后生成的 props(最终转为 SSR 属性字符串)。


6.1 输入框 <input>

    if (node.tag === 'input') {
      const type = findProp(node, 'type')
      if (type) {
        const value = findValueBinding(node)
(1) 动态类型输入框
        if (type.type === NodeTypes.DIRECTIVE) {
          res.ssrTagParts = [
            createCallExpression(context.helper(SSR_RENDER_DYNAMIC_MODEL), [
              type.exp!,
              model,
              value,
            ]),
          ]

SSR_RENDER_DYNAMIC_MODEL 处理,例如:<input :type="inputType" v-model="x">
SSR 阶段会生成动态 type 分支逻辑。

(2) 静态类型输入框

根据 type 值不同:

  • radio
case 'radio':
  res.props = [
    createObjectProperty(
      `checked`,
      createCallExpression(context.helper(SSR_LOOSE_EQUAL), [model, value]),
    ),
  ]
  • checkbox
case 'checkbox':
  const trueValueBinding = findProp(node, 'true-value')
  ...
  res.props = [
    createObjectProperty(
      `checked`,
      createConditionalExpression(
        createCallExpression(`Array.isArray`, [model]),
        createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [model, value]),
        model,
      ),
    ),
  ]
  • file
case 'file':
  context.onError(
    createDOMCompilerError(
      DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
      dir.loc,
    ),
  )
  • default(text/password等)
default:
  checkDuplicatedValue()
  res.props = defaultProps

6.2 文本域 <textarea>

    } else if (node.tag === 'textarea') {
      checkDuplicatedValue()
      node.children = [createInterpolation(model, model.loc)]

替换内部内容为插值表达式,使得 SSR 输出 <textarea>内容</textarea>


6.3 下拉框 <select>

    } else if (node.tag === 'select') {
      processSelectChildren(node.children)

调用前文定义的递归函数,为 <option> 自动添加 selected


6.4 非法用法处理

    } else {
      context.onError(
        createDOMCompilerError(
          DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
          dir.loc,
        ),
      )
    }

例如在 <div><span> 上使用 v-model


6.5 返回结果

    return res
  } else {
    // component v-model
    return transformModel(dir, node, context)
  }

对组件节点交由通用的 transformModel 处理。


7. 辅助函数:提取 value 绑定

function findValueBinding(node: PlainElementNode): ExpressionNode {
  const valueBinding = findProp(node, 'value')
  return valueBinding
    ? valueBinding.type === NodeTypes.DIRECTIVE
      ? valueBinding.exp!
      : createSimpleExpression(valueBinding.value!.content, true)
    : createSimpleExpression(`null`, false)
}

若节点有 v-bind:value 或静态 value 属性,返回其表达式;否则返回一个空表达式 null


四、对比分析

类型 客户端编译 (transformModel) SSR 编译 (ssrTransformModel)
输出形式 动态绑定 + 事件 静态字符串生成
目标 运行时 DOM 更新 初始 HTML 渲染
处理时机 运行时 编译时
表单类型支持 同步运行时 DOM 预渲染选中与值

SSR 版本的核心区别是:它必须在没有 DOM 的环境下模拟“选中/输入”状态


五、实践应用

假设模板如下:

<select v-model="lang">
  <option value="en">English</option>
  <option value="jp">Japanese</option>
</select>

SSR 编译后会生成:

<select>
  <option value="en" selected>English</option>
  <option value="jp">Japanese</option>
</select>

如果 lang === 'en',则 selected 会在服务器端直接输出。
客户端 hydration 阶段无需再额外处理选中逻辑。


六、拓展:与 v-bindv-if 的协同

在 SSR 编译中,v-modelv-bindv-ifv-for 可交织存在。
ssrTransformModel 特意支持递归遍历 v-ifv-for 子节点,确保所有动态渲染的选项都被正确判断和输出。


七、潜在问题与设计思考

  1. 动态类型的复杂性
    <input :type="inputType" v-model="x"> 在 SSR 中必须生成多分支渲染逻辑,否则无法确定 checked/value 的生成规则。
  2. 数组绑定的兼容性
    通过 Array.isArray() 判断是否是多选绑定,这种动态检测在 SSR 环境中需谨慎处理性能开销。
  3. hydration 对齐问题
    SSR 输出的初始状态必须与客户端初始 data 完全一致,否则会出现 mismatch 警告。

八、总结

ssrTransformModel 是 Vue SSR 编译体系中最关键的指令处理逻辑之一。
它在编译阶段将 v-model 的双向绑定语义转化为静态属性字符串,从而实现初始状态可还原、无运行时依赖的 HTML 输出

这段代码充分体现了 Vue 在 编译期与运行期分层设计 的思想:

编译期静态生成 + 运行时动态绑定 = 高性能与高一致性并存。


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

Vue SSR 源码解析:ssrProcessIf 条件渲染的服务端转换逻辑

作者 excel
2025年11月9日 08:06

在 Vue 的服务端渲染(SSR)编译阶段,v-if / v-else-if / v-else 指令需要被转换为可在服务器端执行的渲染逻辑,以生成正确的 HTML 输出。
本文将深入解析 ssrProcessIf 的源码结构、原理设计、与编译端差异,并逐步讲解如何生成对应的 SSR 代码节点。


一、概念层:ssrProcessIf 的职责定位

在 Vue 的编译流程中:

  • 第一阶段(AST 构建) :通过 createStructuralDirectiveTransform 注册结构性指令(如 v-ifv-for)。
  • 第二阶段(SSR 转换) :根据 AST 节点生成服务端可执行代码段。

ssrProcessIf 属于第二阶段函数,其主要任务是:

将编译器 DOM 层的 IfNode(即 v-ifv-else-ifv-else)转换为 SSR 环境下可执行的 JavaScript 条件语句(if / else if / else),并构建对应的渲染逻辑块(BlockStatement)。


二、原理层:源码解构与逻辑流程

完整源码如下(附详细注释):

import {
  type BlockStatement,
  type IfBranchNode,
  type IfNode,
  type NodeTransform,
  NodeTypes,
  createBlockStatement,
  createCallExpression,
  createIfStatement,
  createStructuralDirectiveTransform,
  processIf,
} from '@vue/compiler-dom'
import {
  type SSRTransformContext,
  processChildrenAsStatement,
} from '../ssrCodegenTransform'

// (1) 注册指令 transform,用于第一阶段 AST 构建
export const ssrTransformIf: NodeTransform = createStructuralDirectiveTransform(
  /^(?:if|else|else-if)$/, // 匹配三种 v-if 指令
  processIf, // 使用 compiler-dom 中的基础逻辑
)

// (2) SSR 阶段:根据 AST 生成服务端代码结构
export function ssrProcessIf(
  node: IfNode,
  context: SSRTransformContext,
  disableNestedFragments = false,
  disableComment = false,
): void {
  const [rootBranch] = node.branches

  // 2.1 生成第一个 if 语句
  const ifStatement = createIfStatement(
    rootBranch.condition!,
    processIfBranch(rootBranch, context, disableNestedFragments),
  )
  context.pushStatement(ifStatement)

  // 2.2 遍历后续的 else-if / else 分支
  let currentIf = ifStatement
  for (let i = 1; i < node.branches.length; i++) {
    const branch = node.branches[i]
    const branchBlockStatement = processIfBranch(
      branch,
      context,
      disableNestedFragments,
    )
    if (branch.condition) {
      // else-if 分支
      currentIf = currentIf.alternate = createIfStatement(
        branch.condition,
        branchBlockStatement,
      )
    } else {
      // else 分支
      currentIf.alternate = branchBlockStatement
    }
  }

  // 2.3 无 else 分支时插入空注释节点
  if (!currentIf.alternate && !disableComment) {
    currentIf.alternate = createBlockStatement([
      createCallExpression(`_push`, ['`<!---->`']), // 输出空注释占位
    ])
  }
}

// (3) 处理单个分支的内部 children
function processIfBranch(
  branch: IfBranchNode,
  context: SSRTransformContext,
  disableNestedFragments = false,
): BlockStatement {
  const { children } = branch
  const needFragmentWrapper =
    !disableNestedFragments &&
    (children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
    // 优化:单一子节点为 ForNode 时可跳过 Fragment 包裹
    !(children.length === 1 && children[0].type === NodeTypes.FOR)
  return processChildrenAsStatement(branch, context, needFragmentWrapper)
}

🧩 核心逻辑分层解析

层次 功能说明
ssrTransformIf 注册 AST 转换插件,识别所有 v-if 指令。
ssrProcessIf 将 AST 的 IfNode 转换为 SSR 语句树(JavaScript 逻辑块)。
processIfBranch 将分支的子节点转换为渲染语句体,自动判断是否需要 <template> Fragment 包裹。
_push('<!-- -->') 无匹配分支时,输出 SSR 空注释(与客户端渲染一致)。

三、对比层:SSR vs Client 编译逻辑

项目 客户端编译(compiler-dom 服务端编译(compiler-ssr
输出目标 渲染函数(_createVNode 等) 字符串拼接输出(_push() 调用)
条件控制 通过 createConditionalExpression 生成三元表达式 通过 createIfStatement 生成实际 JS if/else 语句
空分支处理 输出 null 输出 HTML 注释 <!---->
Fragment 包裹 依赖 runtime 渲染优化 在编译阶段判断是否合并 Fragment

总结
客户端编译偏向运行时动态决策(虚拟 DOM diff),
而 SSR 编译是静态化、预展开的逻辑树,追求“可直接输出字符串”的高效性。


四、实践层:示例推演

假设我们有如下模板:

<div>
  <div v-if="ok">A</div>
  <div v-else-if="maybe">B</div>
  <div v-else>C</div>
</div>

经过 ssrProcessIf 转换后,内部会生成伪代码结构如下:

if (ok) {
  _push(`<div>A</div>`)
} else if (maybe) {
  _push(`<div>B</div>`)
} else {
  _push(`<div>C</div>`)
}

若缺省 v-else,则输出:

if (ok) {
  _push(`<div>A</div>`)
} else {
  _push(`<!---->`)
}

→ 这种方式保证了 SSR 输出的 HTML 结构与客户端渲染保持一致。


五、拓展层:相关模块联动

  • processChildrenAsStatement
    负责将模板的子节点编译为 BlockStatement,以便在 SSR 环境中可按顺序 _push()

  • SSRTransformContext
    上下文对象,包含:

    • _push() 输出流管理;
    • 静态/动态片段收集;
    • 嵌套片段(Fragment)处理开关;
  • createIfStatement
    内部封装为标准的 AST IfStatement,确保输出 JS 可被后续 Codegen 阶段直接序列化为字符串。


六、潜在问题与优化方向

  1. 多层嵌套条件的代码量膨胀

    • SSR 输出的是纯 JS 控制流,会导致分支较多时体积增长。
    • 可考虑后续阶段引入“条件预计算”或“短路优化”。
  2. Fragment 包裹判断逻辑复杂

    • 当前通过节点类型和数量判断,边界情况(如 v-if 内包裹 template)可能出现过包或漏包。
  3. 可调试性弱

    • SSR 阶段生成的 _push 调用在调试时可读性差,未来可考虑引入 Source Map 或结构化渲染树调试器。

总结

ssrProcessIf 是 Vue SSR 编译器中处理条件渲染的关键模块。
它通过静态化展开条件逻辑、生成 JS 控制流语句,实现了与客户端一致的输出结果。
这一设计体现了 “运行时动态性 → 编译期确定性” 的 Vue SSR 架构哲学。


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

深入理解 Vue SSR 中的 v-for 编译逻辑:ssrProcessFor 源码解析

作者 excel
2025年11月9日 08:04

一、概念背景

在 Vue 3 的服务端渲染(SSR)编译阶段,v-for 指令的处理过程被拆分为两个阶段:

  1. 第一阶段(结构化转换) :通过 createStructuralDirectiveTransform 捕获模板中的 v-for 语法,并生成语法树(AST)。
  2. 第二阶段(SSR 专用代码生成) :使用 ssrProcessFor 将语法树节点转化为 SSR 运行时可执行的渲染函数片段。

换句话说,ssrProcessFor 是 SSR 编译中 负责把 v-for 从 AST 转换为字符串拼接逻辑 的核心函数。


二、源码结构总览

import {
  type ForNode,
  type NodeTransform,
  NodeTypes,
  createCallExpression,
  createForLoopParams,
  createFunctionExpression,
  createStructuralDirectiveTransform,
  processFor,
} from '@vue/compiler-dom'
import {
  type SSRTransformContext,
  processChildrenAsStatement,
} from '../ssrCodegenTransform'
import { SSR_RENDER_LIST } from '../runtimeHelpers'

// 第一阶段:v-for 的结构化指令转换
export const ssrTransformFor: NodeTransform =
  createStructuralDirectiveTransform('for', processFor)

// 第二阶段:v-for 的 SSR 代码生成逻辑
export function ssrProcessFor(
  node: ForNode,
  context: SSRTransformContext,
  disableNestedFragments = false,
): void {
  const needFragmentWrapper =
    !disableNestedFragments &&
    (node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT)
  const renderLoop = createFunctionExpression(
    createForLoopParams(node.parseResult),
  )
  renderLoop.body = processChildrenAsStatement(
    node,
    context,
    needFragmentWrapper,
  )

  if (!disableNestedFragments) {
    context.pushStringPart(`<!--[-->`)
  }
  context.pushStatement(
    createCallExpression(context.helper(SSR_RENDER_LIST), [
      node.source,
      renderLoop,
    ]),
  )
  if (!disableNestedFragments) {
    context.pushStringPart(`<!--]-->`)
  }
}

三、原理拆解

1. 第一阶段:结构化指令转换

export const ssrTransformFor: NodeTransform =
  createStructuralDirectiveTransform('for', processFor)

这段代码调用了 Vue 编译器的核心工具 createStructuralDirectiveTransform
其作用是:

  • 捕获模板中所有带 v-for 的节点
  • 调用 processFor 将其转换为一个 ForNode AST 节点。

这一阶段仅构建静态结构,不关心 SSR 逻辑。


2. 第二阶段:SSR 渲染逻辑生成

接下来,ssrProcessFor 会在 SSR transform pass 中被调用,将 ForNode 转化为最终的服务端字符串拼接逻辑。

(1) 判断是否需要 Fragment 包裹

const needFragmentWrapper =
  !disableNestedFragments &&
  (node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT)
  • v-for 内有多个子节点,或子节点不是单个元素时,需要用注释标记的 fragment 包裹,以在 SSR 输出中维持正确的层级结构。
  • 例如:
<div v-for="i in list">
  <span>{{ i }}</span>
  <p>text</p>
</div>

会被包裹为:

<!--[--><span>...</span><p>...</p><!--]-->

(2) 构造循环函数表达式

const renderLoop = createFunctionExpression(
  createForLoopParams(node.parseResult),
)
  • createForLoopParamsv-for="(item, index) in list" 中提取参数:(item, index)

  • createFunctionExpression 则生成类似:

    (item, index) => { /* 渲染逻辑 */ }
    

(3) 生成循环体逻辑

renderLoop.body = processChildrenAsStatement(
  node,
  context,
  needFragmentWrapper,
)
  • processChildrenAsStatement 会把 v-for 的子节点转化为 SSR 输出语句;
  • 其中会递归调用 SSR 版本的 processElementprocessText 等;
  • 如果 needFragmentWrapper = true,则会在输出中插入注释节点包裹子节点。

(4) 生成最终 SSR 调用表达式

context.pushStatement(
  createCallExpression(context.helper(SSR_RENDER_LIST), [
    node.source,
    renderLoop,
  ]),
)

生成的代码逻辑大致等价于:

_ssrRenderList(list, (item, index) => {
  // renderLoop.body 逻辑
})

SSR_RENDER_LIST 是 Vue SSR 运行时的辅助函数,作用与客户端渲染中的 renderList 相同,用于在 SSR 阶段执行循环渲染。


(5) 处理 Fragment 包裹标记

if (!disableNestedFragments) {
  context.pushStringPart(`<!--[-->`)
}
// ...loop...
if (!disableNestedFragments) {
  context.pushStringPart(`<!--]-->`)
}
  • 这两行分别在循环输出前后插入特殊注释标记;

  • 这些标记帮助客户端 hydration 过程正确定位 Fragment 边界;

  • 例如 SSR 输出:

    <!--[--><div>...</div><div>...</div><!--]-->
    

四、核心逻辑流程图

v-for AST 节点
    ↓
ssrProcessFor()
    ↓
判断是否需要 Fragment
    ↓
构造 renderLoop 函数表达式
    ↓
将子节点转换为可执行语句块
    ↓
包装 SSR_RENDER_LIST 调用
    ↓
输出字符串标记 + 渲染结果

五、与客户端编译对比

维度 客户端编译 SSR 编译
输出目标 Virtual DOM 渲染函数 字符串拼接逻辑
运行时 Helper renderList SSR_RENDER_LIST
Fragment 用 VNode 包裹 用注释节点包裹
Hydration 需求 不存在 必须维持 DOM 边界一致性
子节点处理 生成虚拟节点数组 转换为字符串输出逻辑

可以看出,SSR 的 v-for 处理逻辑重点在于字符串输出正确性与 Hydration 边界维护,而非虚拟节点构造。


六、实践案例

示例模板

<ul>
  <li v-for="(item, i) in list">{{ item }}</li>
</ul>

SSR 编译结果(简化后)

_ssrRenderList(_ctx.list, (item, i) => {
  return `<li>${_ssrInterpolate(item)}</li>`
})

在服务端渲染时,该函数返回拼接好的字符串数组,最终生成 HTML。


七、拓展思考

  1. disableNestedFragments 参数的用途

    • 用于嵌套结构中(如 v-for 内部的 v-if),避免重复包裹。
    • 例如模板层已经添加了 Fragment 注释,就可以禁用内部 fragment。
  2. SSR_RENDER_LIST 的执行机制

    • 在运行时执行循环,拼接字符串;
    • 同时维持顺序和索引一致,确保 hydration 对应关系。
  3. 性能优化方向

    • 可针对静态 list 进行编译期展开;
    • 在长列表中通过分块渲染减少内存占用。

八、潜在问题与调试要点

问题 可能原因 解决思路
SSR 输出结构不匹配 缺少 Fragment 注释边界 检查 needFragmentWrapper 判断逻辑
Hydration 错位 子节点顺序或注释标识错误 确认 <!--[--><!--]--> 对应位置
性能下降 动态表达式复杂 可使用 key 优化 diff 逻辑
嵌套循环出错 参数作用域冲突 检查 createForLoopParams 是否正确生成参数

九、总结

ssrProcessFor 是 Vue SSR 编译管线中将模板循环指令转换为字符串渲染逻辑的关键模块。
其核心工作包括:

  • 生成循环函数表达式;
  • 处理子节点渲染;
  • 管理 Fragment 包裹与注释边界;
  • 最终生成基于 SSR_RENDER_LIST 的可执行输出。

它是 Vue SSR 保证模板在客户端复水时结构精确对应的重要机制之一。


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

深度解析:Vue 3 中 ssrTransformTransitionGroup 的实现原理与机制

作者 excel
2025年11月9日 08:02

在 Vue 3 的服务端渲染(SSR)编译体系中,TransitionGroup 是一个非常特殊的组件。
它既是一个过渡容器,又需要在服务端生成结构化 HTML,并在客户端保持可 hydration 的一致性。
为了实现这一目标,Vue 编译器对它进行了专门的两阶段处理——
也就是本文要深入解析的 ssrTransformTransitionGroupssrProcessTransitionGroup


一、背景与整体概念

Vue 的 SSR 编译过程与普通编译(即 DOM 渲染编译)不同。
在 SSR 模式下,模板需要被转换为可直接输出 HTML 字符串的渲染函数

对于一般组件来说,SSR 编译器会自动将 props、children 等信息编译为字符串。
但对于 <TransitionGroup> 这种动态包装容器组件,则必须特殊处理:

  • 它允许开发者通过 tag:tag 自定义包裹元素;
  • 它会在运行时将子节点展平(flatten)
  • 它会在运行时过滤注释节点
  • 同时还要保证 SSR 与 Hydration 阶段 DOM 结构对齐。

于是 Vue 内部引入了一个双阶段设计:

Phase 1:构建阶段(Transform)
Phase 2:输出阶段(Process)

这就是本文件实现的核心逻辑。


二、源码全貌

以下是 Vue 源码中的关键实现(简化版):

export function ssrTransformTransitionGroup(node, context) {
  return (): void => {
    const tag = findProp(node, 'tag')
    if (tag) {
      const otherProps = node.props.filter(p => p !== tag)
      const { props, directives } = buildProps(node, context, otherProps, true, false, true)
      let propsExp = null
      if (props || directives.length) {
        propsExp = createCallExpression(context.helper(SSR_RENDER_ATTRS), [
          buildSSRProps(props, directives, context),
        ])
      }
      wipMap.set(node, {
        tag,
        propsExp,
        scopeId: context.scopeId || null,
      })
    }
  }
}

export function ssrProcessTransitionGroup(node, context) {
  const entry = wipMap.get(node)
  if (entry) {
    const { tag, propsExp, scopeId } = entry
    if (tag.type === NodeTypes.DIRECTIVE) {
      // 动态 tag
      context.pushStringPart(`<`)
      context.pushStringPart(tag.exp!)
      if (propsExp) context.pushStringPart(propsExp)
      if (scopeId) context.pushStringPart(` ${scopeId}`)
      context.pushStringPart(`>`)

      processChildren(node, context, false, true, true)

      context.pushStringPart(`</`)
      context.pushStringPart(tag.exp!)
      context.pushStringPart(`>`)
    } else {
      // 静态 tag
      context.pushStringPart(`<${tag.value!.content}`)
      if (propsExp) context.pushStringPart(propsExp)
      if (scopeId) context.pushStringPart(` ${scopeId}`)
      context.pushStringPart(`>`)

      processChildren(node, context, false, true, true)
      context.pushStringPart(`</${tag.value!.content}>`)
    }
  } else {
    // 无 tag 情况
    processChildren(node, context, true, true, true)
  }
}

三、原理剖析:SSR 编译的两阶段逻辑

1. Transform 阶段(ssrTransformTransitionGroup

在 AST 转换阶段,Vue 编译器会遍历模板中的每一个节点。
对于 <TransitionGroup>,这一步主要做的是信息提取

核心流程

const tag = findProp(node, 'tag') // 找出 <TransitionGroup tag="ul"> 的 tag 属性
const otherProps = node.props.filter(p => p !== tag) // 过滤掉 tag
const { props, directives } = buildProps(...) // 构建 SSR props

逻辑说明

  • findProp:查找 <TransitionGroup> 上的 tag 属性;

  • buildProps:构建 SSR 需要的 props;

    • 这里传入的 true /* ssr (skip event listeners) */ 表示跳过事件监听,因为 SSR 不绑定事件;
  • createCallExpression:生成 _ssrRenderAttrs() 调用表达式,用于在后续拼接字符串时插入属性字符串;

  • 最终结果存入一个 WeakMap 缓存,以便第二阶段使用。

生成的中间数据结构

{
  tag: AttributeNode | DirectiveNode,
  propsExp: CallExpression | null,
  scopeId: string | null
}

2. Process 阶段(ssrProcessTransitionGroup

这一阶段真正执行“渲染字符串输出”。

动态标签(:tag="expr")逻辑

if (tag.type === NodeTypes.DIRECTIVE) {
  context.pushStringPart(`<`)
  context.pushStringPart(tag.exp!)
  if (propsExp) context.pushStringPart(propsExp)
  if (scopeId) context.pushStringPart(` ${scopeId}`)
  context.pushStringPart(`>`)
  processChildren(node, context, false, true, true)
  context.pushStringPart(`</`)
  context.pushStringPart(tag.exp!)
  context.pushStringPart(`>`)
}

在这里,tag.exp! 是一个动态表达式(例如 "listTag"),
SSR 输出会变成类似:

<${listTag} ...attrs>...</${listTag}>

静态标签(tag="ul")逻辑

context.pushStringPart(`<${tag.value!.content}`)
if (propsExp) context.pushStringPart(propsExp)
if (scopeId) context.pushStringPart(` ${scopeId}`)
context.pushStringPart(`>`)
processChildren(node, context, false, true, true)
context.pushStringPart(`</${tag.value!.content}>`)

输出类似:

<ul class="fade-list"> ... </ul>

特殊参数说明

processChildren(node, context, false, true, true) 的最后两个布尔参数非常关键:

  • flattenFragments = true:表示将所有子节点展开为单层 fragment;
  • ignoreComments = true:TransitionGroup 会在运行时过滤掉注释节点,因此 SSR 也必须同步。

四、对比与特性分析

特性项 普通组件 Transition TransitionGroup
输出结构 固定 DOM 或 Fragment 包裹子元素 动态 tag 可配置
Props 处理 正常 正常 跳过事件监听
子节点渲染 保持层级 单层 强制展平 (flatten)
注释节点 保留 保留 过滤掉注释节点
SSR 输出 静态 动态包裹 动态拼接标签结构

这一表格揭示了为什么 Vue 需要专门的 SSR transform 逻辑来处理它:
TransitionGroup 同时具备“容器”与“动态结构”的特性。


五、拓展理解:SSR Transform 的模式化设计

Vue 的 SSR Transform 系统普遍遵循一个模板化的模式:

阶段 函数命名 作用
Transform 阶段 ssrTransformXxx 只负责收集静态信息,记录到 WeakMap
Process 阶段 ssrProcessXxx 读取缓存信息,生成最终字符串输出

同类文件包括:

  • ssrTransformComponent
  • ssrTransformTeleport
  • ssrTransformSlotOutlet

这种设计具有以下优势:

  1. 逻辑解耦:Transform 与输出生成分离;
  2. 性能优化:避免重复属性分析;
  3. 递归安全:支持嵌套结构(如 TransitionGroup 内再嵌套组件);
  4. Hydration 一致性:保证 SSR 输出与客户端渲染结构对齐。

六、潜在问题与实现注意事项

  1. WeakMap 生命周期
    编译阶段缓存仅存于内存,若存在并行编译(如 Vite 多线程 SSR),需避免交叉访问。

  2. 属性拼接安全性
    由于 SSR 拼接字符串直接输出 HTML,必须保证 tag.exp 安全,防止 XSS 注入。Vue 内部对该表达式有 AST 级验证。

  3. 作用域 ID 拼接细节
    注意 context.pushStringPart( ${scopeId}) 前的空格,
    若省略,可能导致属性粘连错误,如:

    <ulclass="fade">  // ❌ 错误输出
    
  4. 无 tag 情况
    若开发者未指定 tag,Vue 会退回 fragment 模式(即不包裹任何标签),
    此时调用 processChildren(..., true, true, true)


七、总结与启示

ssrTransformTransitionGroup 是 Vue 3 SSR 架构中一个小而精妙的片段。
它的实现体现出 Vue 对编译阶段与运行时一致性的极高要求:

  • 利用 两阶段编译策略 解决动态结构问题;
  • AST Transform 层 提前抽取信息,避免生成期重复运算;
  • 通过 processChildren 的参数控制,完美模拟了运行时行为。

这种精细化设计让 Vue 的 SSR 输出既高效又精准,保证了前后端渲染结构一致性。


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

Vue SSR 源码解读:ssrTransformTransition 与 ssrProcessTransition 的实现逻辑

作者 excel
2025年11月9日 07:58

在 Vue 3 的服务端渲染(SSR)体系中,Transition 组件虽然在客户端负责动画过渡,但在服务端它并不执行动画,而是仅作为一种逻辑容器。本文将深入分析 Vue SSR 编译阶段如何处理 <transition> 节点,重点讲解 ssrTransformTransitionssrProcessTransition 两个核心函数的作用和设计。


一、概念

1. 背景

当我们在模板中使用:

<transition appear>
  <div>Hello</div>
</transition>

客户端渲染时,Vue 会为其绑定动画钩子。但 SSR 中没有动画执行环境,因此需要一种方式“识别并简化” <transition> 的渲染逻辑,以便最终输出稳定的 HTML 字符串。

2. 目标

SSR 的编译阶段要做到两点:

  • 保留 <transition> 的语义结构;
  • 过滤掉与动画无关的运行时逻辑,只保留必要的内容。

二、原理解析

Vue 的 SSR 编译管线中,针对组件会调用一系列 transformprocess 阶段函数。

  • ssrTransform* 系列函数:负责“分析”组件节点的属性与结构;
  • ssrProcess* 系列函数:负责“生成”最终的 SSR 输出。

对于 <transition>,Vue 分别定义了:

  1. ssrTransformTransition:在 AST 转换阶段执行,用于标记 appear 属性;
  2. ssrProcessTransition:在生成 SSR 代码阶段执行,用于输出最终 HTML。

三、源码拆解与逐行注释

源码整体

import {
  type ComponentNode,
  NodeTypes,
  type TransformContext,
  findProp,
} from '@vue/compiler-dom'
import {
  type SSRTransformContext,
  processChildren,
} from '../ssrCodegenTransform'

const wipMap = new WeakMap<ComponentNode, Boolean>()

export function ssrTransformTransition(
  node: ComponentNode,
  context: TransformContext,
) {
  return (): void => {
    const appear = findProp(node, 'appear', false, true)
    wipMap.set(node, !!appear)
  }
}

export function ssrProcessTransition(
  node: ComponentNode,
  context: SSRTransformContext,
): void {
  // #5351: filter out comment children inside transition
  node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)

  const appear = wipMap.get(node)
  if (appear) {
    context.pushStringPart(`<template>`)
    processChildren(node, context, false, true)
    context.pushStringPart(`</template>`)
  } else {
    processChildren(node, context, false, true)
  }
}

(1)wipMap:工作中间态存储

const wipMap = new WeakMap<ComponentNode, Boolean>()

解释:
这是一个“弱映射表”,用来在 transformprocess 阶段之间共享节点状态。
在编译管线中,同一个节点会多次被访问,WeakMap 能安全地存储它的中间状态,不影响垃圾回收。


(2)ssrTransformTransition:标记阶段

export function ssrTransformTransition(
  node: ComponentNode,
  context: TransformContext,
) {
  return (): void => {
    const appear = findProp(node, 'appear', false, true)
    wipMap.set(node, !!appear)
  }
}

逐行解释:

  • findProp(node, 'appear', false, true):在当前组件节点中查找 appear 属性;
  • 若存在,则表示该 <transition> 有初次动画逻辑;
  • wipMap.set(node, !!appear):将布尔结果记录到 wipMap 中,供后续处理阶段使用。

核心作用:
这一步不生成任何输出,而是为节点打上“是否含 appear” 的标记


(3)ssrProcessTransition:生成阶段

export function ssrProcessTransition(
  node: ComponentNode,
  context: SSRTransformContext,
): void {
  node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)
  const appear = wipMap.get(node)

  if (appear) {
    context.pushStringPart(`<template>`)
    processChildren(node, context, false, true)
    context.pushStringPart(`</template>`)
  } else {
    processChildren(node, context, false, true)
  }
}

逐行解释:

  1. 过滤注释节点:

    node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)
    

    避免无意义的注释干扰 SSR 输出。

  2. 读取 transform 阶段标记:

    const appear = wipMap.get(node)
    

    获取 <transition> 是否含有 appear 属性。

  3. 按条件输出:

    • 如果存在 appear:包裹一层 <template> 标签(表示这是一个初始渲染的容器);
    • 否则,直接输出子节点。
    if (appear) {
      context.pushStringPart(`<template>`)
      processChildren(node, context, false, true)
      context.pushStringPart(`</template>`)
    } else {
      processChildren(node, context, false, true)
    }
    

总结逻辑:
SSR 版本的 <transition> 实际不会生成 <transition> 标签,而是根据是否“初始出现动画”来选择是否包裹 <template>


四、对比:客户端 vs 服务端

环境 Transition 行为 输出结构
客户端 (DOM) 生成真实 <transition> 节点;控制过渡动画 <transition><div>...</div></transition>
服务端 (SSR) 仅输出子内容,不涉及动画逻辑 <div>...</div><template>...</template>

SSR 的 Transition 更像是“语义占位符”,保证编译一致性,而非真实 DOM 控制器。


五、实践案例

示例模板

<transition appear>
  <p>Hello SSR</p>
</transition>

SSR 编译输出(简化后)

<template><p>Hello SSR</p></template>

若去掉 appear 属性:

<transition>
  <p>Hello SSR</p>
</transition>

输出则变为:

<p>Hello SSR</p>

可见,<template> 的出现与 appear 属性一一对应。


六、拓展:WeakMap 的作用与替代方案

使用 WeakMap 而非普通 Map 的原因:

  • 避免内存泄漏(节点销毁后自动释放);
  • 符合“编译阶段数据暂存”的设计原则;
  • 多阶段共享同一引用对象。

若改用普通 Map,节点对象可能被持久引用,导致无法被垃圾回收。


七、潜在问题与思考

  1. SSR 与 Hydration 差异
    SSR 输出 <template> 可能与客户端虚拟 DOM 结构不同,需确保 hydration 阶段 DOM 匹配。
  2. TransitionGroup 的特殊性
    目前逻辑仅覆盖 <transition>,而 <transition-group> 会有更复杂的子节点管理逻辑。
  3. 进一步优化
    若未来引入 v-showv-if 动画 SSR 兼容,可考虑在 transform 阶段做更细粒度 AST 分析。

八、总结

本文从编译管线视角,完整分析了 Vue SSR 如何处理 <transition> 节点:

  • ssrTransformTransition 负责标记;
  • ssrProcessTransition 负责输出;
  • WeakMap 作为中间态桥梁;
  • 最终目标是输出干净、语义一致的 HTML。

这种分阶段设计兼顾了 编译阶段清晰性运行时性能,体现了 Vue SSR 的工程化思路。


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

Vue SSR 深度解析:ssrProcessTeleport 的源码机制与实现原理

作者 excel
2025年11月9日 07:56

在 Vue 3 的服务端渲染(SSR)编译阶段中,ssrProcessTeleport 是一个二次编译(second-pass)阶段的代码生成转换函数,用于处理 <teleport> 组件的服务端输出逻辑。
本文将深入剖析其设计目的、实现原理与编译链中的位置,并通过逐行注释展示源码的运行流程。


一、概念背景:SSR 与 Teleport 的特殊性

Teleport 的核心作用是在客户端渲染时允许开发者将某些内容渲染到 DOM 树的其他位置,例如:

<teleport to="#modal">
  <div>Modal content</div>
</teleport>

而在 SSR(Server-Side Rendering) 模式中,Vue 必须在生成 HTML 字符串时保留这种结构的逻辑信息,以便在客户端 hydrate 时仍能正确关联目标节点。
因此,SSR 编译器必须捕获 teleport 的目标 (to 属性)、内容及禁用状态 (disabled),并生成可在运行时执行的渲染函数调用。


二、原理剖析:函数结构与核心流程

我们先看完整函数结构:

export function ssrProcessTeleport(
  node: ComponentNode,
  context: SSRTransformContext,
): void {
  // 1. 提取 to 属性
  const targetProp = findProp(node, 'to')
  if (!targetProp) {
    context.onError(
      createSSRCompilerError(SSRErrorCodes.X_SSR_NO_TELEPORT_TARGET, node.loc),
    )
    return
  }

  // 2. 解析 teleport 的目标表达式
  let target: ExpressionNode | undefined
  if (targetProp.type === NodeTypes.ATTRIBUTE) {
    target =
      targetProp.value && createSimpleExpression(targetProp.value.content, true)
  } else {
    target = targetProp.exp
  }
  if (!target) {
    context.onError(
      createSSRCompilerError(
        SSRErrorCodes.X_SSR_NO_TELEPORT_TARGET,
        targetProp.loc,
      ),
    )
    return
  }

  // 3. 检查 disabled 属性
  const disabledProp = findProp(node, 'disabled', false, true)
  const disabled = disabledProp
    ? disabledProp.type === NodeTypes.ATTRIBUTE
      ? `true`
      : disabledProp.exp || `false`
    : `false`

  // 4. 生成内容渲染函数
  const contentRenderFn = createFunctionExpression(
    [`_push`],
    undefined,
    true,
    false,
    node.loc,
  )
  contentRenderFn.body = processChildrenAsStatement(node, context)

  // 5. 调用 SSR_RENDER_TELEPORT helper 输出最终代码
  context.pushStatement(
    createCallExpression(context.helper(SSR_RENDER_TELEPORT), [
      `_push`,
      contentRenderFn,
      target,
      disabled,
      `_parent`,
    ]),
  )
}

三、逐行解析与代码注释

1. 依赖导入部分

import {
  type ComponentNode,
  type ExpressionNode,
  NodeTypes,
  createCallExpression,
  createFunctionExpression,
  createSimpleExpression,
  findProp,
} from '@vue/compiler-dom'
  • 这些来自 @vue/compiler-dom 的工具帮助我们在 AST 层面分析节点结构。
  • findProp 用于查找节点上的属性。
  • createSimpleExpression 用于包装字面量。
  • createFunctionExpressioncreateCallExpression 用于生成可序列化的函数调用表达式。
import {
  type SSRTransformContext,
  processChildrenAsStatement,
} from '../ssrCodegenTransform'
  • SSRTransformContext 记录当前的编译状态(例如 helper 函数、输出缓冲区等)。
  • processChildrenAsStatement 会将组件的子节点转换为 _push 调用序列(即生成 HTML 的部分)。

2. 目标属性提取与校验

const targetProp = findProp(node, 'to')
if (!targetProp) {
  context.onError(
    createSSRCompilerError(SSRErrorCodes.X_SSR_NO_TELEPORT_TARGET, node.loc),
  )
  return
}

🔍 Teleport 没有 to 属性时直接报错,因为无法确定渲染目标。


3. 生成 Teleport 目标表达式

let target: ExpressionNode | undefined
if (targetProp.type === NodeTypes.ATTRIBUTE) {
  target =
    targetProp.value && createSimpleExpression(targetProp.value.content, true)
} else {
  target = targetProp.exp
}
  • to 是静态字符串时(如 "body"),会转换成简单表达式;
  • 若为动态绑定(如 :to="dynamicTarget"),则直接使用已存在的表达式。
if (!target) {
  context.onError(
    createSSRCompilerError(
      SSRErrorCodes.X_SSR_NO_TELEPORT_TARGET,
      targetProp.loc,
    ),
  )
  return
}

再次进行容错检查,确保目标有效。


4. 解析 Teleport 的 disabled 属性

const disabledProp = findProp(node, 'disabled', false, true)
const disabled = disabledProp
  ? disabledProp.type === NodeTypes.ATTRIBUTE
    ? `true`
    : disabledProp.exp || `false`
  : `false`

这里实现了对 <teleport disabled>:disabled="isOff" 等多种写法的兼容。
若完全未声明则默认为 "false"


5. 生成内容渲染函数

const contentRenderFn = createFunctionExpression(
  [`_push`],
  undefined,
  true,
  false,
  node.loc,
)
contentRenderFn.body = processChildrenAsStatement(node, context)
  • 这里定义了一个函数 ( _push ) => { ... },其中 _push 是 SSR 生成字符串的累积器。
  • 通过 processChildrenAsStatement 将子节点转换为 _push('<div>...</div>') 的序列。

6. 生成最终的 Teleport 渲染调用

context.pushStatement(
  createCallExpression(context.helper(SSR_RENDER_TELEPORT), [
    `_push`,
    contentRenderFn,
    target,
    disabled,
    `_parent`,
  ]),
)

这一步实际上会生成类似如下的 SSR 代码:

_ssrRenderTeleport(_push, (_push) => {
  _push(`<div>Modal content</div>`)
}, "#modal", false, _parent)

SSR_RENDER_TELEPORT 是运行时 helper,用来在服务器渲染时输出占位结构并记录 Teleport 的上下文。


四、与客户端编译逻辑的对比

模式 渲染位置 主要职责
客户端编译 (compiler-dom) <teleport> 转换为运行时组件调用 负责 DOM 操作与挂载目标
SSR 编译 (compiler-ssr) 生成 _ssrRenderTeleport 调用 负责输出 HTML 字符串结构

SSR 编译器的目标不是运行组件逻辑,而是预先生成字符串模板,因此它会将 Teleport 的运行逻辑“降级”为字符串拼接函数调用。


五、实践:如何调试与扩展

如果你想在自定义 SSR 环境中注入额外逻辑(例如记录 Teleport 使用次数),可以在 ssrCodegenTransform 阶段拦截:

context.registerHelper(SSR_RENDER_TELEPORT)

并在运行时自定义 _ssrRenderTeleport

export function ssrRenderTeleport(push, renderContent, target, disabled, parent) {
  console.log(`Teleport to: ${target}`)
  if (!disabled) {
    renderContent(push)
  }
}

六、拓展思考

  • 可插拔性设计:Vue SSR 的 transform 阶段是模块化的,可针对组件类型注册不同的二次处理函数。
  • AST 级编译复用:此逻辑复用 compiler-dom 的节点定义体系,使得 SSR 与 DOM 编译器高度兼容。
  • 运行时与编译时解耦:SSR 编译器不会直接生成 HTML,而是生成运行时 helper 调用,使得服务器端逻辑更灵活。

七、潜在问题与注意事项

  1. 动态目标表达式的求值:SSR 不会实际解析 :to 绑定的值,必须在运行时环境中确定;
  2. disabled 的字符串化陷阱:在 SSR 生成的代码中 "false" 是字符串,不是真布尔;
  3. Hydration 差异:服务端输出必须与客户端 Teleport 的挂载位置一致,否则 hydration 失败;
  4. 嵌套 Teleport 场景:需要谨慎处理多层 Teleport,否则会引发输出顺序不一致。

八、结语

ssrProcessTeleport 展示了 Vue SSR 编译器的强大与优雅设计:
它以最小的代价在编译阶段保留 Teleport 的运行语义,同时通过抽象层(helper + context)确保代码可维护性与扩展性。

一句话总结:它是将“客户端结构指令”转译为“服务端字符串指令”的桥梁。


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

Vue SSR 源码解析:ssrTransformSuspense 与 ssrProcessSuspense

作者 excel
2025年11月9日 07:41

一、背景与概念说明

Vue 在服务端渲染(SSR)过程中,会对组件模板进行两阶段编译:

  • 阶段一(Transform) :生成用于描述结构的中间表达(IR, Intermediate Representation)。
  • 阶段二(Codegen) :将中间表达转换为最终的字符串拼接指令(例如 _push_renderSlot)。

<Suspense> 组件是 Vue 3 的一个特殊机制,用于异步内容加载与占位渲染。
在 SSR 环境下,Vue 需要为 <Suspense> 生成可在服务端正确处理异步与 fallback(回退内容)的渲染逻辑。


二、源码概览

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

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

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 代码结构大致如下:

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

❌
❌