一、概念背景
在 Vue 3 的服务端渲染(SSR)编译阶段,v-for 指令的处理过程被拆分为两个阶段:
- 第一阶段(结构化转换) :通过
createStructuralDirectiveTransform捕获模板中的v-for语法,并生成语法树(AST)。 - 第二阶段(SSR 专用代码生成) :使用
ssrProcessFor将语法树节点转化为 SSR 运行时可执行的渲染函数片段。
换句话说,ssrProcessFor 是 SSR 编译中 负责把 v-for 从 AST 转换为字符串拼接逻辑 的核心函数。
二、源码结构总览
typescript
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. 第一阶段:结构化指令转换
arduino
export const ssrTransformFor: NodeTransform =
createStructuralDirectiveTransform('for', processFor)
这段代码调用了 Vue 编译器的核心工具 createStructuralDirectiveTransform。
其作用是:
- 捕获模板中所有带
v-for的节点; - 调用
processFor将其转换为一个ForNodeAST 节点。
这一阶段仅构建静态结构,不关心 SSR 逻辑。
2. 第二阶段:SSR 渲染逻辑生成
接下来,ssrProcessFor 会在 SSR transform pass 中被调用,将 ForNode 转化为最终的服务端字符串拼接逻辑。
(1) 判断是否需要 Fragment 包裹
ini
const needFragmentWrapper =
!disableNestedFragments &&
(node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT)
- 当
v-for内有多个子节点,或子节点不是单个元素时,需要用注释标记的 fragment 包裹,以在 SSR 输出中维持正确的层级结构。 - 例如:
css
<div v-for="i in list">
<span>{{ i }}</span>
<p>text</p>
</div>
会被包裹为:
xml
<!--[--><span>...</span><p>...</p><!--]-->
(2) 构造循环函数表达式
ini
const renderLoop = createFunctionExpression(
createForLoopParams(node.parseResult),
)
-
createForLoopParams从v-for="(item, index) in list"中提取参数:(item, index); -
createFunctionExpression则生成类似:perl(item, index) => { /* 渲染逻辑 */ }
(3) 生成循环体逻辑
ini
renderLoop.body = processChildrenAsStatement(
node,
context,
needFragmentWrapper,
)
processChildrenAsStatement会把v-for的子节点转化为 SSR 输出语句;- 其中会递归调用 SSR 版本的
processElement、processText等; - 如果
needFragmentWrapper = true,则会在输出中插入注释节点包裹子节点。
(4) 生成最终 SSR 调用表达式
less
context.pushStatement(
createCallExpression(context.helper(SSR_RENDER_LIST), [
node.source,
renderLoop,
]),
)
生成的代码逻辑大致等价于:
javascript
_ssrRenderList(list, (item, index) => {
// renderLoop.body 逻辑
})
SSR_RENDER_LIST 是 Vue SSR 运行时的辅助函数,作用与客户端渲染中的 renderList 相同,用于在 SSR 阶段执行循环渲染。
(5) 处理 Fragment 包裹标记
xml
if (!disableNestedFragments) {
context.pushStringPart(`<!--[-->`)
}
// ...loop...
if (!disableNestedFragments) {
context.pushStringPart(`<!--]-->`)
}
-
这两行分别在循环输出前后插入特殊注释标记;
-
这些标记帮助客户端 hydration 过程正确定位 Fragment 边界;
-
例如 SSR 输出:
xml<!--[--><div>...</div><div>...</div><!--]-->
四、核心逻辑流程图
scss
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 边界维护,而非虚拟节点构造。
六、实践案例
示例模板
css
<ul>
<li v-for="(item, i) in list">{{ item }}</li>
</ul>
SSR 编译结果(简化后)
javascript
_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 辅助生成,并由作者整理审核。