vue3源码解析:编译之转换器实现原理

73 阅读11分钟

上文分析了parser的实现,包括词法分析和语法分析。经过parser的解析,template字符串就生成了一颗AST。此时的AST的节点上还有vue独有的属性、指令(例如v-for、#slot)等内容,需要经过Transform将这些指令转换成代码片段。同时,做一些标记和优化,便于diff的优化。

一、转换器的设计理念

转换器(Transform)是 Vue 编译系统的核心组件之一,它接收解析器生成的原始 AST,通过一系列转换步骤,生成优化后的 AST。转换器的主要目标是:

  1. 分析模板的静态/动态特性
  2. 标记可优化的节点
  3. 应用各种编译时优化
  4. 为代码生成做准备

二、转换器的核心架构

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]();
  }
}

对于我们的示例,遍历过程大致如下:

  1. 首先处理根节点 <template>
  2. 然后处理 <div> 元素
  3. 处理 <h1> 和其中的插值表达式 {{ title }}
  4. 处理 <ul> 及其 v-if 指令
  5. 处理 <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. 优化效果
  1. 静态内容提升

    • 静态的 class="container" 被提升到 hoists 数组中
    • 原位置被替换为对提升内容的引用(_hoisted_x
  2. 缓存的信息

    • context.hoists:存储所有被提升的静态节点
    • context.helpers:记录需要的辅助函数
    • 每个被提升的节点都有唯一的引用标识符
  3. 性能提升

    • 减少内存占用:静态内容只存储一份
    • 提高渲染性能:不需要重复创建静态节点
    • 优化打包体积:静态内容可以在编译时优化

这种优化对于包含大量静态内容的模板特别有效,因为它可以显著减少运行时的计算和内存开销。通过将静态内容提升到渲染函数之外,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. 主要变化说明
  1. 块转换

    • 单个根节点被转换为块级节点(Block)
    • 添加了 isBlock: true 标记
    • 设置了相应的 patchFlag
  2. Fragment 包装

    • 多个根节点会被 Fragment 包装
    • Fragment 也会被标记为块级节点
    • 使用 STABLE_FRAGMENT 标记优化更新
  3. 优化标记

    • 添加了运行时优化所需的各种标记
    • 包括 patchFlagdynamicProps
    • 这些标记会影响运行时的更新策略
  4. 辅助函数注入

    • 根据需要注入 createBlockcreateFragment 等辅助函数
    • 这些函数会在代码生成阶段被使用

这一步的处理为后续的代码生成做好了准备,通过添加必要的标记和结构,使得生成的代码能够充分利用 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. 插件使用场景

  1. 自定义元素处理
const transformCustomElement: NodeTransform = (node, context) => {
  if (node.type === NodeTypes.ELEMENT && isCustomElement(node.tag)) {
    // 处理自定义元素
  }
};
  1. 特殊指令转换
const transformSpecialDirective: DirectiveTransform = (dir, node, context) => {
  // 处理特殊指令
  return {
    props: [
      createObjectProperty(
        createSimpleExpression("custom", true),
        dir.exp || createSimpleExpression("", true)
      ),
    ],
    needRuntime: false,
  };
};
  1. 代码注入
const injectCode: NodeTransform = (node, context) => {
  if (node.type === NodeTypes.ROOT) {
    context.helper(INJECT_HELPER);
    // 注入自定义代码
  }
};

这种插件系统的设计使得 Vue 编译器具有极强的扩展性,开发者可以:

  • 添加自定义转换逻辑
  • 修改现有的转换行为
  • 注入辅助代码
  • 实现特定的优化策略

六、总结

Vue 编译器的转换器通过精心设计的架构实现了强大的模板优化能力:

  1. 灵活的遍历系统支持复杂的 AST 转换
  2. 高效的静态提升机制提升运行时性能
  3. 完善的表达式处理确保模板正确性
  4. 智能的优化策略最小化运行时开销
  5. 可扩展的插件系统支持自定义转换

这些特性使得 Vue 编译器能够:

  • 生成高效的运行时代码
  • 提供灵活的优化选项
  • 支持复杂的模板特性

在下一篇文章中,我们将分析代码生成器的实现,探讨如何将优化后的 AST 转换为最终的渲染函数。