上文分析了parser的实现,包括词法分析和语法分析。经过parser的解析,template字符串就生成了一颗AST。此时的AST的节点上还有vue独有的属性、指令(例如v-for、#slot)等内容,需要经过Transform将这些指令转换成代码片段。同时,做一些标记和优化,便于diff的优化。
一、转换器的设计理念
转换器(Transform)是 Vue 编译系统的核心组件之一,它接收解析器生成的原始 AST,通过一系列转换步骤,生成优化后的 AST。转换器的主要目标是:
- 分析模板的静态/动态特性
- 标记可优化的节点
- 应用各种编译时优化
- 为代码生成做准备
二、转换器的核心架构
1. 转换上下文
interface TransformContext {
// 编译选项
filename: string; // 文件名
prefixIdentifiers: boolean; // 是否添加标识符前缀
hoistStatic: boolean; // 是否开启静态提升
cacheHandlers: boolean; // 是否缓存事件处理器
nodeTransforms: NodeTransform[]; // 节点转换器数组
directiveTransforms: Record<string, DirectiveTransform>; // 指令转换器映射
// 编译状态
root: RootNode; // AST 根节点
helpers: Map<symbol, number>; // 辅助函数使用计数映射
components: Set<string>; // 组件集合
directives: Set<string>; // 指令集合
hoists: (JSChildNode | null)[]; // 被提升的静态节点
imports: ImportItem[]; // 导入项
temps: number; // 临时变量计数
cached: (CacheExpression | null)[]; // 缓存表达式
// 作用域管理
identifiers: { [name: string]: number | undefined }; // 标识符使用计数
scopes: {
vFor: number; // v-for 作用域计数
vSlot: number; // v-slot 作用域计数
vPre: number; // v-pre 作用域计数
vOnce: number; // v-once 作用域计数
};
// 节点遍历状态
parent: ParentNode | null; // 当前节点的父节点
currentNode: RootNode | TemplateChildNode | null; // 当前处理的节点
childIndex: number; // 当前节点在父节点children中的索引
inVOnce: boolean; // 是否在 v-once 指令内
// 工具方法
helper<T extends symbol>(name: T): T; // 添加辅助函数
removeHelper<T extends symbol>(name: T): void; // 移除辅助函数
helperString(name: symbol): string; // 获取辅助函数字符串
replaceNode(node: TemplateChildNode): void; // 替换当前节点
removeNode(node?: TemplateChildNode): void; // 移除节点
hoist(exp: JSChildNode): SimpleExpressionNode; // 静态提升表达式
}
2. 转换流程管理
// 主转换函数
function transform(root: RootNode, options: TransformOptions): void {
// 1. 创建转换上下文
const context = createTransformContext(root, options);
// 2. 遍历并转换 AST
traverseNode(root, context);
// 3. 如果开启了静态提升,执行静态节点缓存
if (options.hoistStatic) {
cacheStatic(root, context);
}
// 4. 如果不是 SSR 模式,创建根节点代码生成节点
if (!options.ssr) {
createRootCodegen(root, context);
}
// 5. 完成转换,收集最终信息
root.helpers = new Set([...context.helpers.keys()]);
root.components = [...context.components];
root.directives = [...context.directives];
root.imports = context.imports;
root.hoists = context.hoists;
root.temps = context.temps;
root.cached = context.cached;
}
三、核心转换步骤
让我们通过一个具体的例子来分析 Vue 编译器的核心转换流程。假设我们有以下模板:
<template>
<div class="container">
<h1>{{ title }}</h1>
<ul v-if="items.length">
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
这个模板经过解析器处理后,会生成一个初始的 AST。让我们跟踪这个 AST 是如何被转换的。
1. 节点遍历与转换
首先,transform 函数会调用 traverseNode 开始遍历 AST:
// 遍历节点的主函数
function traverseNode(
node: RootNode | TemplateChildNode,
context: TransformContext
): void {
// 1. 设置当前处理的节点
context.currentNode = node;
// 2. 应用转换插件
const { nodeTransforms } = context;
const exitFns = [];
// 2.1 执行所有转换器
for (let i = 0; i < nodeTransforms.length; i++) {
const onExit = nodeTransforms[i](node, context);
if (onExit) {
if (isArray(onExit)) {
exitFns.push(...onExit);
} else {
exitFns.push(onExit);
}
}
// 如果节点被移除,直接返回
if (!context.currentNode) {
return;
} else {
// 节点可能被替换,更新当前节点
node = context.currentNode;
}
}
// 3. 根据节点类型处理子节点
switch (node.type) {
case NodeTypes.INTERPOLATION:
// 处理插值表达式,如 {{ title }}
if (!context.ssr) {
context.helper(TO_DISPLAY_STRING);
}
break;
case NodeTypes.IF:
// 处理 v-if 分支
for (let i = 0; i < node.branches.length; i++) {
traverseNode(node.branches[i], context);
}
break;
case NodeTypes.IF_BRANCH:
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
// 递归处理子节点
traverseChildren(node, context);
break;
}
// 4. 执行退出函数
context.currentNode = node;
let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}
对于我们的示例,遍历过程大致如下:
- 首先处理根节点
<template> - 然后处理
<div>元素 - 处理
<h1>和其中的插值表达式{{ title }} - 处理
<ul>及其v-if指令 - 处理
<li>及其v-for指令和插值表达式
2. 子节点遍历
在处理每个节点时,如果遇到容器节点(如元素节点),会通过 traverseChildren 递归处理其子节点:
function traverseChildren(parent: ParentNode, context: TransformContext): void {
let i = 0;
// 节点被移除时的回调
const nodeRemoved = () => {
i--;
};
// 遍历所有子节点
for (; i < parent.children.length; i++) {
const child = parent.children[i];
if (isString(child)) continue;
// 设置遍历上下文
context.parent = parent;
context.childIndex = i;
context.onNodeRemoved = nodeRemoved;
// 递归遍历子节点
traverseNode(child, context);
}
}
经过遍历处理后,我们的 AST 会被转换成如下结构:
{
type: NodeTypes.ROOT,
children: [{
type: NodeTypes.ELEMENT,
tag: 'div',
props: [{ name: 'class', value: 'container' }],
children: [
{
type: NodeTypes.ELEMENT,
tag: 'h1',
children: [{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'title'
}
}]
},
{
type: NodeTypes.IF,
condition: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'items.length'
},
branches: [{
type: NodeTypes.ELEMENT,
tag: 'ul',
children: [{
type: NodeTypes.FOR,
source: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'items'
},
valueAlias: 'item',
children: [/* ... */]
}]
}]
}
]
}]
}
3. 静态节点缓存
遍历完成后,如果开启了静态提升优化,会调用 cacheStatic 函数处理静态节点:
function cacheStatic(root: RootNode, context: TransformContext) {
// 1. 遍历 AST 寻找静态节点
walk(root, context, new Map());
// 2. 标记可缓存的节点
if (context.hoists.length) {
context.helper(CREATE_STATIC);
}
}
function walk(node: ASTNode, context: TransformContext): boolean {
// 检查节点是否可以被静态提升
if (isStaticNode(node)) {
// 生成静态节点的渲染函数
const staticCall = createStaticVNodeCall(node, context);
// 添加到提升列表
context.hoists.push(staticCall);
// 替换原节点为引用
const hoistIndex = context.hoists.length - 1;
node.codegenNode = createSimpleExpression(
`_hoisted_${hoistIndex}`,
false,
node.loc,
ConstantTypes.CAN_CACHE
);
return true;
}
return false;
}
让我们看看经过 cacheStatic 处理后,AST 的具体变化。以我们的示例模板为例:
<template>
<div class="container">
<h1>{{ title }}</h1>
<ul v-if="items.length">
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
1. 静态节点的识别和提升
原始的 AST 结构:
{
type: NodeTypes.ROOT,
children: [{
type: NodeTypes.ELEMENT,
tag: 'div',
props: [
// 这是静态的,可以被提升
{ name: 'class', value: 'container' }
],
children: [...]
}]
}
经过 cacheStatic 处理后:
{
type: NodeTypes.ROOT,
// 新增 hoists 数组,存储被提升的静态节点
hoists: [
createVNodeCall(
context,
CREATE_STATIC,
[`class="container"`], // 静态属性被序列化
undefined,
undefined
)
],
children: [{
type: NodeTypes.ELEMENT,
tag: 'div',
// 原来的静态属性被替换为引用
codegenNode: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: '_hoisted_0', // 引用提升后的静态节点
isStatic: false,
constType: ConstantTypes.CAN_CACHE
}
}]
}
2. 缓存的信息
在 TransformContext 中记录的信息:
context: {
// 1. 提升的静态节点列表
hoists: [
// div 的 class 属性
createStaticVNodeCall(/*...*/),
// 其他静态内容
],
// 2. 需要的辅助函数
helpers: Map {
CREATE_STATIC => 1, // 用于创建静态节点
CREATE_ELEMENT_BLOCK => 1, // 用于创建块级元素
// ...其他辅助函数
}
}
3. 优化后的完整结构
{
type: NodeTypes.ROOT,
children: [{
type: NodeTypes.ELEMENT,
tag: 'div',
// 静态属性被提升
props: '_hoisted_0',
children: [
{
type: NodeTypes.ELEMENT,
tag: 'h1',
// 动态内容保持不变
children: [{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'title'
}
}]
},
// v-if 和 v-for 部分因为是动态的,所以保持不变
{
type: NodeTypes.IF,
// ...
}
]
}]
}
4. 优化效果
-
静态内容提升:
- 静态的 class="container" 被提升到
hoists数组中 - 原位置被替换为对提升内容的引用(
_hoisted_x)
- 静态的 class="container" 被提升到
-
缓存的信息:
context.hoists:存储所有被提升的静态节点context.helpers:记录需要的辅助函数- 每个被提升的节点都有唯一的引用标识符
-
性能提升:
- 减少内存占用:静态内容只存储一份
- 提高渲染性能:不需要重复创建静态节点
- 优化打包体积:静态内容可以在编译时优化
这种优化对于包含大量静态内容的模板特别有效,因为它可以显著减少运行时的计算和内存开销。通过将静态内容提升到渲染函数之外,Vue 确保这些内容只会被创建一次,并在后续的渲染中被重复使用。
4. 根节点代码生成
最后,通过 createRootCodegen 为根节点创建代码生成节点:
function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context;
const { children } = root;
// 处理单个子节点的情况
if (children.length === 1) {
const child = children[0];
if (isSingleElementRoot(root, child) && child.codegenNode) {
const codegenNode = child.codegenNode;
if (codegenNode.type === NodeTypes.VNODE_CALL) {
// 转换为块
convertToBlock(codegenNode, context);
}
root.codegenNode = codegenNode;
} else {
root.codegenNode = child;
}
}
// 处理多个子节点的情况
else if (children.length > 1) {
// 创建 Fragment
root.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
root.children,
PatchFlags.STABLE_FRAGMENT,
undefined,
undefined,
true
);
}
}
让我们继续看看经过 createRootCodegen 处理后,AST 的变化。以我们的示例为例:
1. 单个根节点的情况
对于我们的示例模板:
<template>
<div class="container">
<!-- 内容 -->
</div>
</template>
处理前的 AST:
{
type: NodeTypes.ROOT,
children: [{
type: NodeTypes.ELEMENT,
tag: 'div',
props: '_hoisted_0', // 已经被静态提升的属性
children: [/* ... */],
}]
}
处理后的 AST:
{
type: NodeTypes.ROOT,
children: [/* 保持不变 */],
// 新增 codegenNode,转换为块级调用
codegenNode: {
type: NodeTypes.VNODE_CALL,
tag: 'div',
props: '_hoisted_0',
children: [/* ... */],
patchFlag: PatchFlags.STABLE_FRAGMENT,
dynamicProps: undefined,
directives: undefined,
isBlock: true, // 标记为块
disableTracking: false,
isComponent: false
}
}
2. 多个根节点的情况
如果模板有多个根节点:
<template>
<h1>{{ title }}</h1>
<div class="content">
<!-- 内容 -->
</div>
</template>
处理前的 AST:
{
type: NodeTypes.ROOT,
children: [
{
type: NodeTypes.ELEMENT,
tag: 'h1',
children: [/* ... */]
},
{
type: NodeTypes.ELEMENT,
tag: 'div',
props: [{ name: 'class', value: 'content' }],
children: [/* ... */]
}
]
}
处理后的 AST:
{
type: NodeTypes.ROOT,
children: [/* 保持不变 */],
// 新增 codegenNode,使用 Fragment 包裹多个根节点
codegenNode: {
type: NodeTypes.VNODE_CALL,
tag: FRAGMENT,
props: undefined,
children: [
/* 原始子节点数组 */
],
patchFlag: PatchFlags.STABLE_FRAGMENT,
dynamicProps: undefined,
directives: undefined,
isBlock: true,
disableTracking: false,
isComponent: false
}
}
3. 主要变化说明
-
块转换:
- 单个根节点被转换为块级节点(Block)
- 添加了
isBlock: true标记 - 设置了相应的
patchFlag
-
Fragment 包装:
- 多个根节点会被 Fragment 包装
- Fragment 也会被标记为块级节点
- 使用
STABLE_FRAGMENT标记优化更新
-
优化标记:
- 添加了运行时优化所需的各种标记
- 包括
patchFlag、dynamicProps等 - 这些标记会影响运行时的更新策略
-
辅助函数注入:
- 根据需要注入
createBlock、createFragment等辅助函数 - 这些函数会在代码生成阶段被使用
- 根据需要注入
这一步的处理为后续的代码生成做好了准备,通过添加必要的标记和结构,使得生成的代码能够充分利用 Vue 的运行时优化机制。
四、优化策略
Vue 编译器的转换器实现了多种优化策略,让我们详细分析每种策略:
1. 静态提升 (Static Hoisting)
// 开启静态提升的配置
const options: TransformOptions = {
hoistStatic: true,
// ...其他配置
};
// 静态提升的实现
function hoistStatic(root: RootNode, context: TransformContext) {
walk(root, context, new Map());
if (context.hoists.length) {
context.helper(CREATE_STATIC);
}
}
优化效果:
- 将静态内容提升到渲染函数外部
- 避免每次渲染时重新创建
- 减少内存占用和 GC 压力
2. 补丁标记 (Patch Flags)
// 补丁标记的类型
export const enum PatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 1 << 1, // 动态类名
STYLE = 1 << 2, // 动态样式
PROPS = 1 << 3, // 动态属性
FULL_PROPS = 1 << 4, // 需要完整 diff 的属性
HYDRATE_EVENTS = 1 << 5, // 事件监听器
STABLE_FRAGMENT = 1 << 6, // 稳定序列
KEYED_FRAGMENT = 1 << 7, // 带 key 的序列
UNKEYED_FRAGMENT = 1 << 8, // 无 key 的序列
NEED_PATCH = 1 << 9, // 需要补丁的节点
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
DEV_ROOT_FRAGMENT = 1 << 11, // 开发环境根片段
}
优化效果:
- 精确标记动态内容类型
- 运行时可以跳过静态内容的比对
- 针对不同类型的更新采用不同的优化策略
3. 块级树结构 (Block Tree)
function convertToBlock(node: VNodeCall, context: TransformContext) {
if (node.isBlock) {
return;
}
context.helper(CREATE_BLOCK);
node.isBlock = true;
// 设置补丁标记
if (node.children.length > 1) {
node.patchFlag |= PatchFlags.STABLE_FRAGMENT;
}
// 处理动态子节点
if (hasDynamicChildren(node)) {
node.dynamicChildren = [];
}
}
优化效果:
- 将模板分割成块级单元
- 追踪动态子节点
- 减少不必要的 DOM 遍历和比对
4. 事件缓存 (Event Caching)
// 事件处理器缓存的配置
const options: TransformOptions = {
cacheHandlers: true,
// ...其他配置
};
// 事件处理器转换
function transformOn(
dir: DirectiveNode,
node: ElementNode,
context: TransformContext
) {
if (context.cacheHandlers) {
// 生成缓存的处理器
const handler = createCacheHandler(dir.exp, context);
context.cached.push(handler);
}
}
优化效果:
- 避免每次渲染时重新创建事件处理器
- 减少不必要的事件监听器注册和移除
- 优化内存使用
五、插件系统
Vue 编译器的转换器提供了强大的插件系统,允许自定义转换逻辑。
1. 转换插件的类型
// 节点转换插件
type NodeTransform = (
node: RootNode | TemplateChildNode,
context: TransformContext
) => void | (() => void) | (() => void)[];
// 指令转换插件
type DirectiveTransform = (
dir: DirectiveNode,
node: ElementNode,
context: TransformContext
) => DirectiveTransformResult;
2. 插件注册机制
function createTransformContext(
root: RootNode,
options: TransformOptions
): TransformContext {
return {
// ...其他上下文属性
// 注册节点转换插件
nodeTransforms: [
transformElement,
transformText,
...(options.nodeTransforms || []),
],
// 注册指令转换插件
directiveTransforms: {
on: transformOn,
bind: transformBind,
...options.directiveTransforms,
},
};
}
3. 插件执行流程
function traverseNode(
node: RootNode | TemplateChildNode,
context: TransformContext
) {
// 1. 收集退出函数
const exitFns: (() => void)[] = [];
// 2. 按顺序执行转换插件
for (const transform of context.nodeTransforms) {
const onExit = transform(node, context);
if (onExit) {
exitFns.push(onExit);
}
// 检查节点是否被移除
if (!context.currentNode) {
return;
}
}
// 3. 处理子节点
traverseChildren(node, context);
// 4. 反序执行退出函数
for (let i = exitFns.length - 1; i >= 0; i--) {
exitFns[i]();
}
}
4. 自定义插件示例
// 自定义节点转换插件
const customTransform: NodeTransform = (node, context) => {
// 进入节点时的处理
if (node.type === NodeTypes.ELEMENT) {
// 转换逻辑
}
// 返回退出函数
return () => {
// 退出节点时的处理
};
};
// 自定义指令转换插件
const customDirectiveTransform: DirectiveTransform = (dir, node, context) => {
return {
props: [],
needRuntime: false,
};
};
// 注册插件
const options: TransformOptions = {
nodeTransforms: [customTransform],
directiveTransforms: {
custom: customDirectiveTransform,
},
};
5. 插件使用场景
- 自定义元素处理:
const transformCustomElement: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && isCustomElement(node.tag)) {
// 处理自定义元素
}
};
- 特殊指令转换:
const transformSpecialDirective: DirectiveTransform = (dir, node, context) => {
// 处理特殊指令
return {
props: [
createObjectProperty(
createSimpleExpression("custom", true),
dir.exp || createSimpleExpression("", true)
),
],
needRuntime: false,
};
};
- 代码注入:
const injectCode: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ROOT) {
context.helper(INJECT_HELPER);
// 注入自定义代码
}
};
这种插件系统的设计使得 Vue 编译器具有极强的扩展性,开发者可以:
- 添加自定义转换逻辑
- 修改现有的转换行为
- 注入辅助代码
- 实现特定的优化策略
六、总结
Vue 编译器的转换器通过精心设计的架构实现了强大的模板优化能力:
- 灵活的遍历系统支持复杂的 AST 转换
- 高效的静态提升机制提升运行时性能
- 完善的表达式处理确保模板正确性
- 智能的优化策略最小化运行时开销
- 可扩展的插件系统支持自定义转换
这些特性使得 Vue 编译器能够:
- 生成高效的运行时代码
- 提供灵活的优化选项
- 支持复杂的模板特性
在下一篇文章中,我们将分析代码生成器的实现,探讨如何将优化后的 AST 转换为最终的渲染函数。