普通视图
Vue SFC 编译器源码深度解析:processDefineEmits 与运行时事件生成机制
本文将深入剖析 Vue 单文件组件(SFC)编译器中的 defineEmits 处理逻辑,来自 compiler-sfc 模块的源码实现。
本文涵盖从概念、原理到实践的全链路分析,辅以详细注释与代码逐行解析。
一、概念:defineEmits 的定位与作用
在 Vue 3 的 <script setup> 中,defineEmits() 用于声明组件可以触发的事件。
例如:
const emit = defineEmits(['submit', 'cancel'])
emit('submit')
其核心目标是:
- 类型层面:提供 TypeScript 类型约束(确保事件名称正确)。
-
运行时层面:生成可用的
emits配置,供 Vue 组件选项使用。
在编译阶段,Vue 会分析 defineEmits() 的调用,提取其中的事件定义或类型信息,并生成最终的运行时代码。
二、原理:编译器的处理流程总览
defineEmits 的编译处理分为三个核心步骤:
-
检测与记录调用:
processDefineEmits- 识别
defineEmits()调用语句; - 检查重复定义;
- 区分运行时参数和类型参数;
- 保存到上下文(
ScriptCompileContext)。
- 识别
-
提取运行时声明:
extractRuntimeEmits- 若使用 TypeScript 类型参数(
defineEmits<{...}>()),提取其中的事件名称。
- 若使用 TypeScript 类型参数(
-
生成最终运行时代码:
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"])
}
六、拓展:与 defineProps、defineModel 的关系
-
defineProps:声明组件输入接口; -
defineEmits:声明组件输出事件; -
defineModel:语法糖,自动生成props+emits的绑定关系。
genRuntimeEmits 之所以会检测 hasDefineModelCall,正是为了保证双向绑定事件正确注入。
七、潜在问题与限制
-
混用类型与运行时参数报错
编译时强制互斥,否则抛出错误。 -
函数签名与对象类型混合报错
例如:defineEmits<{ submit: () => void (e: 'cancel'): void }>()这种混用是不被允许的。
-
无法动态计算事件名
所有事件名称必须为字面量常量,否则类型提取失败。
八、总结
processDefineEmits 是 Vue SFC 编译过程中负责 事件定义提取 的关键函数。
它结合类型系统与运行时声明,生成统一的 emits 数组,保证组件在类型安全与运行时行为之间的平衡。
核心价值:
- 静态分析事件声明;
- 支持 TypeScript 类型提取;
- 自动合并
v-model相关事件。
本文部分内容借助 AI 辅助生成,并由作者整理审核。
🚀 从 GPT-5 流式输出看现代前端的流式请求机制(Koa 实现版)
一、前言:为什么要“流式输出”?
传统 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"));
🧠 说明:
-
ctx.set("Content-Type", "text/event-stream")告诉浏览器使用 SSE 流式响应; - 每个
ctx.res.write()会立即发送一部分内容; - 客户端不需要多次请求,而是持续读取同一个响应流。
五、前端如何接收流
前端用原生 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 源码全解
一、背景与概念
在 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)))
}
🧩 逐行解析
-
创建上下文:
const context = createSSRTransformContext(ast, options)创建一个 SSR 专用的转换上下文,用于存储编译状态(包括 helper、body、错误回调等)。
-
注入 CSS 变量:
if (options.ssrCssVars) { ... }当模板中使用了 SFC
<style>中定义的 CSS 变量时,需要生成_cssVars常量,确保 SSR 渲染时能正确解析。 -
判断是否为 Fragment:
const isFragment = ast.children.length > 1 && ast.children.some(c => !isText(c))若根节点包含多个子节点(或非纯文本节点),需将其包裹为
<!--[-->与<!--]-->片段标记。 -
核心递归处理:
processChildren(ast, context, isFragment)将 AST 中的所有子节点转换为 JS 表达式或字符串片段。
-
生成最终代码块:
ast.codegenNode = createBlockStatement(context.body)以 BlockStatement(JS 语法树节点)形式输出整个 SSR 渲染函数体。
-
区分 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(`<!--]-->`)
}
🧩 核心逻辑分解
-
Fragment 包裹:
使用注释节点标记多节点片段,便于客户端 hydration 对齐。 -
节点类型分派:
-
元素节点:委托给
ssrProcessElement()。 -
组件节点:调用
ssrProcessComponent()。 -
插槽节点:使用
ssrProcessSlotOutlet()。 - 文本节点:HTML 转义后直接拼接。
-
插值表达式:调用
_interpolatehelper。 -
条件 / 循环节点:分别交由
ssrProcessIf与ssrProcessFor处理。
-
元素节点:委托给
-
注释节点:
默认输出到最终 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 运行时辅助工具注册机制源码详解
本文将深入解析 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)
核心逻辑解析:
-
Symbol 定义
export const SSR_INTERPOLATE: unique symbol = Symbol(`ssrInterpolate`)- 每个 helper 使用
Symbol()生成独一无二的标识,防止命名冲突。 - TypeScript 的
unique symbol类型确保类型系统能静态识别这些常量。
- 每个 helper 使用
-
映射表(ssrHelpers)
export const ssrHelpers: Record<symbol, string> = { ... }- 将 Symbol 映射到对应字符串名称。
- 这些名称必须与
@vue/server-renderer中的 helper 函数名严格一致,否则在 SSR 构建阶段会发生运行时错误。
-
运行时注册
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)
六、潜在问题与注意事项
-
命名必须与运行时实现严格对应
若ssrHelpers中的字符串与@vue/server-renderer实现不一致,会导致运行时抛出undefined is not a function。 -
SSR 与 CSR Helper 不可混用
SSR helper 仅在服务端使用,客户端 hydration 阶段应依赖客户端 helper。 -
不应在模板中直接引用未注册 helper
模板编译器只识别已注册的 helper,否则编译器无法生成合法代码。
七、总结
本文展示了 Vue SSR 运行时辅助函数的注册机制:
- 通过
unique symbol保证唯一性; - 通过
registerRuntimeHelpers注册至编译器; - 在编译阶段根据模板特性自动注入相应 helper;
- 最终在服务端渲染时调用具体的字符串生成函数。
这种设计实现了 SSR 模块的模块化与灵活性,是 Vue 3 编译器与运行时架构分离思想的典型体现。
本文部分内容借助 AI 辅助生成,并由作者整理审核。
深入解析 Vue SSR 编译器的核心函数:compile
Vue 3 的服务端渲染(SSR, Server-Side Rendering)体系中,compile() 是一个关键函数。它负责将模板字符串或 AST 抽象语法树转化为 可在服务端执行的渲染函数,以生成最终的 HTML 字符串。
本文我们将深入解析 compile 的完整实现,剖析其底层机制与设计哲学。
一、概念:compile 是做什么的?
compile 是 Vue SSR 编译流程的入口函数,功能上类似于前端版本的 @vue/compiler-dom 的 compile,但专为服务端渲染优化。
它主要完成以下几个任务:
- 解析模板(Parsing)→ 生成 AST;
- 执行转换(Transform)→ 为 SSR 注入特定逻辑;
- 生成代码(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: true与inSSR: true→ 明确告诉编译器处于服务端渲染模式; -
prefixIdentifiers: true→ 在 SSR 模式下启用变量前缀(如_ctx.),避免作用域冲突; -
cacheHandlers与hoistStatic被禁用,因为 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.nodeTransforms 或 options.directiveTransforms 注入自定义逻辑,实现个性化的 SSR 编译管线。
六、潜在问题与注意事项
-
与 hydration 不兼容的行为
某些指令如v-once、v-memo无法在 SSR 端使用,会在 hydration 时失效。 -
CSS 变量同步问题
ssrInjectCssVars仅编译注入变量,但客户端需同步以避免闪烁。 -
性能陷阱
若模板过大,generate()阶段可能生成极长字符串;可考虑分块渲染。
总结
compile() 是 Vue SSR 编译器的核心接口,它将模板编译为可执行的字符串生成函数,是从模板到 HTML 的桥梁。
通过多层 transform 管线与 SSR 专用 codegen,Vue 实现了优雅的模板到字符串编译机制。
本文部分内容借助 AI 辅助生成,并由作者整理审核。
Vue SSR 错误系统源码解析:createSSRCompilerError 与 SSRErrorCodes 的设计原理
在 Vue 3 的服务端渲染(Server-Side Rendering, SSR)编译器实现中,错误系统是一个极其关键的组成部分。本文将深入剖析 createSSRCompilerError、SSRErrorCodes 以及相关机制,展示 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 错误系统的扩展点为基准。 - 后续枚举项自增:
66、67。 - 注释中的
__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.
分步解释:
-
创建错误对象:调用
createSSRCompilerError并传入错误码。 -
附加位置信息:在
loc中指定源码范围。 -
错误输出:内部根据
SSRErrorMessages查表生成友好的消息文本。
五、拓展层:Vue 编译器的错误生态体系
SSR 错误系统只是 Vue 编译器错误体系的一环,其他模块包括:
- DOMErrorCodes:处理模板语法与指令问题。
- CompilerError:统一错误接口。
- transform 与 parser:通过上下文(context)传播错误对象。
- onError 回调机制:允许上层(如 Vue Loader)捕获错误并友好提示。
通过这种模块化设计,Vue 编译器能在不同阶段复用通用错误逻辑,并保持类型安全。
六、潜在问题与改进思路
-
枚举值手动同步风险
若 DOM 扩展点更新但 SSR 未调整,将导致测试失败。可考虑使用自动脚本同步。 -
错误码分布不连续
若未来引入更多 SSR 模块,可能需要重新规划错误码段。 -
国际化支持不足
当前错误信息仅英文,未来可引入多语言映射表。
总结
createSSRCompilerError 模块体现了 Vue 编译器体系中严谨的模块分层与扩展策略。
通过枚举、映射表与类型系统的协作,Vue 能在 SSR 编译阶段精准定位错误,保证开发者在调试复杂渲染逻辑时得到清晰反馈。
本文部分内容借助 AI 辅助生成,并由作者整理审核。
Vue SSR 编译器源码深析:ssrTransformShow 的实现原理与设计哲学
一、概念理解: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:指令对象,包含name、exp、modifiers等信息。 -
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)的关键机制。
六、潜在问题与注意事项
-
v-show的动态性缺失
SSR 仅处理初始渲染,后续仍需在客户端由 runtime 更新样式。
若表达式依赖异步数据,则 SSR 阶段无法精确控制。 -
样式覆盖冲突
若模板或 CSS 已设置display样式,SSR 输出的内联样式可能被覆盖。 -
hydration 不一致风险
若 SSR 阶段与客户端初始数据不一致,会造成 hydration mismatch。
总结
ssrTransformShow 是 Vue SSR 编译管线中的一个小而精的组件,其核心使命是:
将“显示控制指令”转译为“样式表达式”,确保 SSR 输出结构完整且可预测。
通过 createConditionalExpression、createObjectExpression 等函数的组合,Vue 实现了“模板 → AST → SSR 表达式”的全链条自动化编译。
本文部分内容借助 AI 辅助生成,并由作者整理审核。
Vue SSR 源码解析:ssrTransformModel 深度剖析
一、概念
在 Vue 的 SSR(服务端渲染)编译阶段中,v-model 指令的处理逻辑与客户端渲染存在显著差异。
客户端的 v-model 依赖运行时双向绑定机制,而 SSR 需要在编译时就生成静态字符串输出,因此必须提供一个对应的 SSR 版本 transform:ssrTransformModel。
它的任务是:
- 将
v-model指令在服务端编译阶段转化为适用于服务端的渲染表达式; - 自动为不同类型的表单控件(
input、textarea、select等)注入正确的 SSR 绑定逻辑; - 确保在模板被渲染成字符串时,
v-model的值反映在 DOM 属性中(例如选中状态或输入值)。
二、原理
ssrTransformModel 是一个 DirectiveTransform,即用于指令编译的转换函数。它通过匹配 v-model 指令节点,根据节点类型(HTML 元素类型)生成对应的 SSR 代码片段。
核心思路如下:
-
判断节点类型
- 普通元素(
input、textarea、select) → 服务端静态输出; - 组件节点(
<MyInput v-model="x" />) → 委托给transformModel()。
- 普通元素(
-
针对不同元素的处理逻辑
-
input[type=text]:输出value属性; -
input[type=radio]:根据v-model值是否匹配当前选项生成checked; -
input[type=checkbox]:根据数组/布尔判断生成checked; -
textarea:将内部内容替换为插值; -
select:递归处理所有option,为选中项添加selected属性。
-
-
错误与校验
-
检查不合法用法,如:
-
v-model与value同时存在; -
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 处理相关工具函数(如
createCallExpression、createConditionalExpression等)。- 后半部分导入 SSR 渲染辅助函数(如
SSR_LOOSE_EQUAL、SSR_RENDER_DYNAMIC_MODEL等),这些是 SSR 运行时在字符串拼接中使用的 helper。
2. 核心 transform 函数定义
export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
const model = dir.exp!
说明:
dir:表示v-model指令节点;node:当前绑定的元素节点;context:编译上下文,用于错误报告、helper 注册等;model:v-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-for或v-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)
}
}
逐步说明:
若
<option>没有显式selected,则判断它是否应被选中;判断逻辑:
- 如果
v-model是数组,则调用SSR_LOOSE_CONTAIN(model, value);- 否则用
SSR_LOOSE_EQUAL(model, value);若判断为真,则拼接字符串
" 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-bind、v-if 的协同
在 SSR 编译中,v-model 与 v-bind、v-if、v-for 可交织存在。ssrTransformModel 特意支持递归遍历 v-if 和 v-for 子节点,确保所有动态渲染的选项都被正确判断和输出。
七、潜在问题与设计思考
-
动态类型的复杂性
<input :type="inputType" v-model="x">在 SSR 中必须生成多分支渲染逻辑,否则无法确定checked/value的生成规则。 -
数组绑定的兼容性
通过Array.isArray()判断是否是多选绑定,这种动态检测在 SSR 环境中需谨慎处理性能开销。 -
hydration 对齐问题
SSR 输出的初始状态必须与客户端初始data完全一致,否则会出现 mismatch 警告。
八、总结
ssrTransformModel 是 Vue SSR 编译体系中最关键的指令处理逻辑之一。
它在编译阶段将 v-model 的双向绑定语义转化为静态属性字符串,从而实现初始状态可还原、无运行时依赖的 HTML 输出。
这段代码充分体现了 Vue 在 编译期与运行期分层设计 的思想:
编译期静态生成 + 运行时动态绑定 = 高性能与高一致性并存。
本文部分内容借助 AI 辅助生成,并由作者整理审核。
Vue SSR 源码解析:ssrProcessIf 条件渲染的服务端转换逻辑
在 Vue 的服务端渲染(SSR)编译阶段,v-if / v-else-if / v-else 指令需要被转换为可在服务器端执行的渲染逻辑,以生成正确的 HTML 输出。
本文将深入解析 ssrProcessIf 的源码结构、原理设计、与编译端差异,并逐步讲解如何生成对应的 SSR 代码节点。
一、概念层:ssrProcessIf 的职责定位
在 Vue 的编译流程中:
-
第一阶段(AST 构建) :通过
createStructuralDirectiveTransform注册结构性指令(如v-if、v-for)。 - 第二阶段(SSR 转换) :根据 AST 节点生成服务端可执行代码段。
ssrProcessIf 属于第二阶段函数,其主要任务是:
将编译器 DOM 层的
IfNode(即v-if、v-else-if、v-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
内部封装为标准的 ASTIfStatement,确保输出 JS 可被后续 Codegen 阶段直接序列化为字符串。
六、潜在问题与优化方向
-
多层嵌套条件的代码量膨胀
- SSR 输出的是纯 JS 控制流,会导致分支较多时体积增长。
- 可考虑后续阶段引入“条件预计算”或“短路优化”。
-
Fragment 包裹判断逻辑复杂
- 当前通过节点类型和数量判断,边界情况(如
v-if内包裹template)可能出现过包或漏包。
- 当前通过节点类型和数量判断,边界情况(如
-
可调试性弱
- SSR 阶段生成的
_push调用在调试时可读性差,未来可考虑引入 Source Map 或结构化渲染树调试器。
- SSR 阶段生成的
总结
ssrProcessIf 是 Vue SSR 编译器中处理条件渲染的关键模块。
它通过静态化展开条件逻辑、生成 JS 控制流语句,实现了与客户端一致的输出结果。
这一设计体现了 “运行时动态性 → 编译期确定性” 的 Vue SSR 架构哲学。
本文部分内容借助 AI 辅助生成,并由作者整理审核。
深入理解 Vue SSR 中的 v-for 编译逻辑:ssrProcessFor 源码解析
一、概念背景
在 Vue 3 的服务端渲染(SSR)编译阶段,v-for 指令的处理过程被拆分为两个阶段:
-
第一阶段(结构化转换) :通过
createStructuralDirectiveTransform捕获模板中的v-for语法,并生成语法树(AST)。 -
第二阶段(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将其转换为一个ForNodeAST 节点。
这一阶段仅构建静态结构,不关心 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),
)
-
createForLoopParams从v-for="(item, index) in list"中提取参数:(item, index); -
createFunctionExpression则生成类似:(item, index) => { /* 渲染逻辑 */ }
(3) 生成循环体逻辑
renderLoop.body = processChildrenAsStatement(
node,
context,
needFragmentWrapper,
)
-
processChildrenAsStatement会把v-for的子节点转化为 SSR 输出语句; - 其中会递归调用 SSR 版本的
processElement、processText等; - 如果
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。
七、拓展思考
-
disableNestedFragments参数的用途- 用于嵌套结构中(如
v-for内部的v-if),避免重复包裹。 - 例如模板层已经添加了 Fragment 注释,就可以禁用内部 fragment。
- 用于嵌套结构中(如
-
SSR_RENDER_LIST的执行机制- 在运行时执行循环,拼接字符串;
- 同时维持顺序和索引一致,确保 hydration 对应关系。
-
性能优化方向
- 可针对静态
list进行编译期展开; - 在长列表中通过分块渲染减少内存占用。
- 可针对静态
八、潜在问题与调试要点
| 问题 | 可能原因 | 解决思路 |
|---|---|---|
| SSR 输出结构不匹配 | 缺少 Fragment 注释边界 | 检查 needFragmentWrapper 判断逻辑 |
| Hydration 错位 | 子节点顺序或注释标识错误 | 确认 <!--[--> 和 <!--]--> 对应位置 |
| 性能下降 | 动态表达式复杂 | 可使用 key 优化 diff 逻辑 |
| 嵌套循环出错 | 参数作用域冲突 | 检查 createForLoopParams 是否正确生成参数 |
九、总结
ssrProcessFor 是 Vue SSR 编译管线中将模板循环指令转换为字符串渲染逻辑的关键模块。
其核心工作包括:
- 生成循环函数表达式;
- 处理子节点渲染;
- 管理 Fragment 包裹与注释边界;
- 最终生成基于
SSR_RENDER_LIST的可执行输出。
它是 Vue SSR 保证模板在客户端复水时结构精确对应的重要机制之一。
本文部分内容借助 AI 辅助生成,并由作者整理审核。
深度解析:Vue 3 中 ssrTransformTransitionGroup 的实现原理与机制
在 Vue 3 的服务端渲染(SSR)编译体系中,TransitionGroup 是一个非常特殊的组件。
它既是一个过渡容器,又需要在服务端生成结构化 HTML,并在客户端保持可 hydration 的一致性。
为了实现这一目标,Vue 编译器对它进行了专门的两阶段处理——
也就是本文要深入解析的 ssrTransformTransitionGroup 与 ssrProcessTransitionGroup。
一、背景与整体概念
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 |
读取缓存信息,生成最终字符串输出 |
同类文件包括:
ssrTransformComponentssrTransformTeleportssrTransformSlotOutlet
这种设计具有以下优势:
- 逻辑解耦:Transform 与输出生成分离;
- 性能优化:避免重复属性分析;
- 递归安全:支持嵌套结构(如 TransitionGroup 内再嵌套组件);
- Hydration 一致性:保证 SSR 输出与客户端渲染结构对齐。
六、潜在问题与实现注意事项
-
WeakMap 生命周期
编译阶段缓存仅存于内存,若存在并行编译(如 Vite 多线程 SSR),需避免交叉访问。 -
属性拼接安全性
由于 SSR 拼接字符串直接输出 HTML,必须保证tag.exp安全,防止 XSS 注入。Vue 内部对该表达式有 AST 级验证。 -
作用域 ID 拼接细节
注意context.pushStringPart(${scopeId})前的空格,
若省略,可能导致属性粘连错误,如:<ulclass="fade"> // ❌ 错误输出 -
无 tag 情况
若开发者未指定 tag,Vue 会退回 fragment 模式(即不包裹任何标签),
此时调用processChildren(..., true, true, true)。
七、总结与启示
ssrTransformTransitionGroup 是 Vue 3 SSR 架构中一个小而精妙的片段。
它的实现体现出 Vue 对编译阶段与运行时一致性的极高要求:
- 利用 两阶段编译策略 解决动态结构问题;
- 在 AST Transform 层 提前抽取信息,避免生成期重复运算;
- 通过 processChildren 的参数控制,完美模拟了运行时行为。
这种精细化设计让 Vue 的 SSR 输出既高效又精准,保证了前后端渲染结构一致性。
结尾说明:
本文部分内容借助 AI 辅助生成,并由作者整理审核。
Vue SSR 源码解读:ssrTransformTransition 与 ssrProcessTransition 的实现逻辑
在 Vue 3 的服务端渲染(SSR)体系中,Transition 组件虽然在客户端负责动画过渡,但在服务端它并不执行动画,而是仅作为一种逻辑容器。本文将深入分析 Vue SSR 编译阶段如何处理 <transition> 节点,重点讲解 ssrTransformTransition 与 ssrProcessTransition 两个核心函数的作用和设计。
一、概念
1. 背景
当我们在模板中使用:
<transition appear>
<div>Hello</div>
</transition>
客户端渲染时,Vue 会为其绑定动画钩子。但 SSR 中没有动画执行环境,因此需要一种方式“识别并简化” <transition> 的渲染逻辑,以便最终输出稳定的 HTML 字符串。
2. 目标
SSR 的编译阶段要做到两点:
- 保留
<transition>的语义结构; - 过滤掉与动画无关的运行时逻辑,只保留必要的内容。
二、原理解析
Vue 的 SSR 编译管线中,针对组件会调用一系列 transform 与 process 阶段函数。
-
ssrTransform*系列函数:负责“分析”组件节点的属性与结构; -
ssrProcess*系列函数:负责“生成”最终的 SSR 输出。
对于 <transition>,Vue 分别定义了:
-
ssrTransformTransition:在 AST 转换阶段执行,用于标记appear属性; -
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>()
解释:
这是一个“弱映射表”,用来在 transform 与 process 阶段之间共享节点状态。
在编译管线中,同一个节点会多次被访问,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)
}
}
逐行解释:
过滤注释节点:
node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)避免无意义的注释干扰 SSR 输出。
读取 transform 阶段标记:
const appear = wipMap.get(node)获取
<transition>是否含有appear属性。按条件输出:
- 如果存在
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,节点对象可能被持久引用,导致无法被垃圾回收。
七、潜在问题与思考
-
SSR 与 Hydration 差异
SSR 输出<template>可能与客户端虚拟 DOM 结构不同,需确保 hydration 阶段 DOM 匹配。 -
TransitionGroup 的特殊性
目前逻辑仅覆盖<transition>,而<transition-group>会有更复杂的子节点管理逻辑。 -
进一步优化
若未来引入v-show、v-if动画 SSR 兼容,可考虑在 transform 阶段做更细粒度 AST 分析。
八、总结
本文从编译管线视角,完整分析了 Vue SSR 如何处理 <transition> 节点:
-
ssrTransformTransition负责标记; -
ssrProcessTransition负责输出; - WeakMap 作为中间态桥梁;
- 最终目标是输出干净、语义一致的 HTML。
这种分阶段设计兼顾了 编译阶段清晰性 与 运行时性能,体现了 Vue SSR 的工程化思路。
本文部分内容借助 AI 辅助生成,并由作者整理审核。
Vue SSR 深度解析:ssrProcessTeleport 的源码机制与实现原理
在 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用于包装字面量。 -
createFunctionExpression与createCallExpression用于生成可序列化的函数调用表达式。
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 调用,使得服务器端逻辑更灵活。
七、潜在问题与注意事项
-
动态目标表达式的求值:SSR 不会实际解析
:to绑定的值,必须在运行时环境中确定; -
disabled 的字符串化陷阱:在 SSR 生成的代码中
"false"是字符串,不是真布尔; - Hydration 差异:服务端输出必须与客户端 Teleport 的挂载位置一致,否则 hydration 失败;
- 嵌套 Teleport 场景:需要谨慎处理多层 Teleport,否则会引发输出顺序不一致。
八、结语
ssrProcessTeleport 展示了 Vue SSR 编译器的强大与优雅设计:
它以最小的代价在编译阶段保留 Teleport 的运行语义,同时通过抽象层(helper + context)确保代码可维护性与扩展性。
一句话总结:它是将“客户端结构指令”转译为“服务端字符串指令”的桥梁。
本文部分内容借助 AI 辅助生成,并由作者整理审核。
Vue SSR 源码解析:ssrTransformSuspense 与 ssrProcessSuspense
一、背景与概念说明
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) 」任务:
- 检测该组件是否有子节点;
- 创建一个
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
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 代码结构大致如下:
_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 辅助生成,并由作者整理审核。