模板编译三阶段:parse-transform-generate

在前面的文章中,我们完整地探索了 Vue3 的运行时系统:响应式、虚拟DOM、渲染器、组件生命周期。但 Vue 还有一个重要的组成部分:编译器。它能将我们写的模板,转换成高效的渲染函数。今天,我们将深入编译器的三个核心阶段:parse、transform、generate。

前言:从模板到 JavaScript

当我们在 Vue 中写出如下代码时:

html 复制代码
<template>
  <div class="container">
    <h1>{{ title }}</h1>
    <p v-if="show">Hello {{ name }}</p>
    <button @click="increment">点击</button>
  </div>
</template>

Vue 最终会把它编译成 JavaScript 代码:

javascript 复制代码
import { openBlock, createBlock, createVNode } from 'vue'

export function render() {
  return (openBlock(), createBlock('div', { class: 'container' }, [
    createVNode('h1', null, ctx.title),
    ctx.show ? createVNode('p', null, 'Hello ' + ctx.name) : null,
    createVNode('button', { onClick: ctx.increment }, '点击')
  ]))
}

这个转换过程,就是模板编译。

编译器在 Vue3 中的定位

什么是编译器

编译器其实只是一段程序,用来将 "语言A" 翻译成 "语言B" 。其中,"语言A" 通常被称为源代码 ,"语言B" 通常被称为目标代码 ,编译器将源代码转换成目标代码的过程叫做编译。

编译过程

完整的编译过程通常包括:

  1. 词法分析
  2. 语法分析
  3. 语义分析
  4. 中间代码生成
  5. 优化
  6. 目标代码生成

其中,前 3 个步骤又被统称为编译前端 ,它们通常与目标平台无关,仅仅负责分析源代码;后 3 个步骤又被称为编译后端,通常与目标平台相关。

注:编译后端的 3 个步骤中,中间代码生成 和 优化 2 个步骤不是必须的,这 2 个步骤也会被统称为中端。

Vue 中的编译器

对 Vue 的编译器而言,其源代码就是组件的模板 <template> , 而目标代码其实就是渲染函数 render 。根据开发阶段和运行阶段的不同,Vue 编辑器又体现出了运行时 与 编译时 的分工区别:

  • 运行时:处理响应式、虚拟 DOM、渲染,此时项目代码体积较大,但无需编译,直接可用
  • 编译时:将模板转换为渲染函数,可以在构建时完成,减小运行时体积

为什么需要编译器?

  • 声明式语法:模板比手写渲染函数更直观
  • 编译优化:静态提升、patch flags等
  • 开发体验:错误提示、语法检查
  • 跨平台:可以编译成不同平台的渲染函数

Vue 编译器结构

  • Parser解析器:将模板字符串解析成模版 AST
  • Transformer转换器:将模板 AST 转换成 JavaScript AST
  • Generator生成器:根据 JavaScript AST 生成渲染函数代码

Parse:将模板字符串转为模板 AST

什么是 AST?

AST(abstract syntax tree),即抽象语法树,是用 JavaScript 对象来描述模板的树形数据结构。比如我们有如下的模板示例:

html 复制代码
<div>
  <h1 v-if="ok">Vue</h1>
</div>

上述模板示例,会被编译成以下的 AST 结构:

javascript 复制代码
const ast = {
  // 逻辑根节点
  type: 'Root',
  children: [
    // div 标签
    {
    		type: 'Element',
    		tag: 'div',
    		children: [
    		  // h1 标签
    		  {
    		    type: 'Element',
    				tag: 'h1',
    				props: [
    				  // v-if 指令
    				  {
    				    type: 'Directive',
    				    name: 'if',  // 指令的直接名称,不能带 v- 前缀
    				    exp: {
    				      type: 'Expression', // 指令表达式
    				      content: 'ok'
    				    }
    				  }
    				]
    		  }
    		]
    }
  ]
}

Parser 设计

Parser 通常采用状态机 的方式,逐个字符扫描模板:

手写实现:简单的模板解析器

javascript 复制代码
/**
 * 简单的模板解析器
 * 将模板字符串解析为AST
 */
class TemplateParser {
  constructor(template) {
    this.template = template;
    this.index = 0;          // 当前解析位置
    this.length = template.length;
  }
  
  /**
   * 解析模板
   */
  parse() {
    const ast = {
      type: 'Root',
      children: []
    };
    
    while (this.index < this.length) {
      const node = this.parseNode();
      if (node) {
        ast.children.push(node);
      }
    }
    
    return ast;
  }
  
  /**
   * 解析单个节点
   */
  parseNode() {
    // 跳过空白
    this.skipWhitespace();
    
    if (this.index >= this.length) return null;
    
    if (this.template[this.index] === '<') {
      // 解析元素
      return this.parseElement();
    } else {
      // 解析文本
      return this.parseText();
    }
  }
  
  /**
   * 解析元素
   */
  parseElement() {
    // 跳过 <
    this.index++;
    
    // 解析标签名
    const tagName = this.parseTagName();
    
    if (!tagName) {
      throw new Error('解析失败:标签名不能为空');
    }
    
    // 解析属性
    const props = this.parseAttributes();
    
    // 检查是否为自闭合标签
    if (this.template[this.index] === '/') {
      this.index += 2; // 跳过 />
      return {
        type: 'Element',
        tag: tagName,
        props,
        isSelfClosing: true,
        children: []
      };
    }
    
    // 跳过 >
    this.index++;
    
    // 解析子节点
    const children = [];
    while (this.index < this.length && !this.matchEndTag(tagName)) {
      const child = this.parseNode();
      if (child) {
        children.push(child);
      }
    }
    
    // 跳过结束标签
    this.parseEndTag(tagName);
    
    return {
      type: 'Element',
      tag: tagName,
      props,
      isSelfClosing: false,
      children
    };
  }
  
  /**
   * 解析标签名
   */
  parseTagName() {
    const start = this.index;
    while (this.index < this.length && /[a-zA-Z0-9\-]/.test(this.template[this.index])) {
      this.index++;
    }
    return this.template.slice(start, this.index);
  }
  
  /**
   * 解析属性
   */
  parseAttributes() {
    const props = [];
    
    while (this.index < this.length) {
      this.skipWhitespace();
      
      // 遇到 > 或 /> 说明属性解析完毕
      if (this.template[this.index] === '>' || 
          (this.template[this.index] === '/' && this.template[this.index + 1] === '>')) {
        break;
      }
      
      // 解析属性名
      const nameStart = this.index;
      while (this.index < this.length && 
             /[a-zA-Z0-9\-:@]/.test(this.template[this.index])) {
        this.index++;
      }
      const name = this.template.slice(nameStart, this.index);
      
      // 解析属性值
      let value = null;
      if (this.template[this.index] === '=') {
        this.index++; // 跳过 =
        
        const quote = this.template[this.index];
        if (quote === '"' || quote === "'") {
          this.index++; // 跳过引号
          const valueStart = this.index;
          while (this.index < this.length && this.template[this.index] !== quote) {
            this.index++;
          }
          value = this.template.slice(valueStart, this.index);
          this.index++; // 跳过结束引号
        }
      }
      
      props.push({
        type: 'Attribute',
        name,
        value
      });
    }
    
    return props;
  }
  
  /**
   * 解析文本
   */
  parseText() {
    const start = this.index;
    
    while (this.index < this.length && this.template[this.index] !== '<') {
      // 检查插值表达式
      if (this.template[this.index] === '{' && 
          this.template[this.index + 1] === '{') {
        break;
      }
      this.index++;
    }
    
    const content = this.template.slice(start, this.index);
    
    if (content.trim()) {
      return {
        type: 'Text',
        content
      };
    }
    
    return null;
  }
  
  /**
   * 检查是否为结束标签
   */
  matchEndTag(tagName) {
    if (this.template[this.index] !== '<') return false;
    if (this.template[this.index + 1] !== '/') return false;
    
    const start = this.index + 2;
    const end = start + tagName.length;
    
    return this.template.slice(start, end) === tagName;
  }
  
  /**
   * 解析结束标签
   */
  parseEndTag(tagName) {
    this.index += 2 + tagName.length; // 跳过 </标签名
    this.skipWhitespace();
    this.index++; // 跳过 >
  }
  
  /**
   * 跳过空白字符
   */
  skipWhitespace() {
    while (this.index < this.length && /\s/.test(this.template[this.index])) {
      this.index++;
    }
  }
}

Vue3 的 AST 结构

Vue3 的 AST 结构实际上要复杂很多,包含了很多信息:

javascript 复制代码
const NodeTypes = {
  ROOT: 0,          // 根节点
  ELEMENT: 1,       // 元素节点
  TEXT: 2,          // 文本节点
  COMMENT: 3,       // 注释节点
  SIMPLE_EXPRESSION: 4,  // 简单表达式
  INTERPOLATION: 5,      // 插值表达式
  ATTRIBUTE: 6,          // 属性
  DIRECTIVE: 7,          // 指令
  // ... 更多类型
};

Transform:转换 AST

Transform 的目的

通过 parser 解析得到的模板 AST 并不能直接用于代码生成,需要转换成 JavaScript AST 之后才能用来生成代码。Transform 过程中,我们需要一些处理:

  • 指令处理:v-if、v-for 等转换为条件表达式
  • 静态提升:标记静态节点
  • 补丁标记:添加 PatchFlags
  • 优化:预计算结果表达式

Transform 设计

Vue3 的 Transform 转换器采用插件化设计:

javascript 复制代码
/**
 * 简单的转换器
 */
class Transformer {
  constructor(ast) {
    this.ast = ast;
    this.plugins = [];
    this.context = {
      currentNode: null,
      parent: null,
      replaceNode: (node) => {
        // 替换节点逻辑
      }
    };
  }
  
  /**
   * 注册插件
   */
  use(plugin) {
    this.plugins.push(plugin);
    return this;
  }
  
  /**
   * 执行转换
   */
  transform() {
    this.traverseNode(this.ast);
    return this.ast;
  }
  
  /**
   * 遍历节点
   */
  traverseNode(node, parent = null) {
    if (!node) return;
    
    // 设置上下文
    this.context.currentNode = node;
    this.context.parent = parent;
    
    // 执行所有插件
    for (const plugin of this.plugins) {
      plugin(node, this.context);
    }
    
    // 递归处理子节点
    if (node.children) {
      for (let i = 0; i < node.children.length; i++) {
        this.traverseNode(node.children[i], node);
      }
    }
    
    // 处理其他属性...
  }
}

// 插件示例:转换插值表达式
const transformInterpolation = (node, context) => {
  if (node.type === 'Text' && node.content.includes('{{')) {
    // 解析插值表达式
    const matches = node.content.match(/\{\{(.*?)\}\}/g);
    
    if (matches) {
      // 将文本节点转换为表达式节点
      const newChildren = [];
      let lastIndex = 0;
      
      for (const match of matches) {
        const start = node.content.indexOf(match, lastIndex);
        const end = start + match.length;
        
        // 添加前面的文本
        if (start > lastIndex) {
          newChildren.push({
            type: 'Text',
            content: node.content.slice(lastIndex, start)
          });
        }
        
        // 添加表达式
        const expr = match.slice(2, -2).trim();
        newChildren.push({
          type: 'Interpolation',
          content: expr
        });
        
        lastIndex = end;
      }
      
      // 添加剩余的文本
      if (lastIndex < node.content.length) {
        newChildren.push({
          type: 'Text',
          content: node.content.slice(lastIndex)
        });
      }
      
      // 替换当前节点
      context.replaceNode(newChildren);
    }
  }
};

// 插件示例:处理v-if指令
const transformVIf = (node, context) => {
  if (node.type === 'Element' && node.props) {
    const vIfProp = node.props.find(p => p.name === 'v-if');
    
    if (vIfProp) {
      // 转换为条件节点
      node.type = 'Conditional';
      node.condition = vIfProp.value;
      node.consequent = node.children;
      node.alternate = null;
      
      // 移除v-if属性
      node.props = node.props.filter(p => p.name !== 'v-if');
    }
  }
};

// 插件示例:静态节点标记
const transformStatic = (node, context) => {
  if (node.type === 'Element') {
    // 检查是否为静态节点
    const hasDynamic = this.checkDynamic(node);
    node.isStatic = !hasDynamic;
    
    if (node.isStatic) {
      node.patchFlag = -1; // HOISTED
    }
  }
};

Vue3 内置的转换插件

javascript 复制代码
const transformPlugins = [
  transformText,           // 文本转换
  transformElement,        // 元素转换
  transformSlotOutlet,     // 插槽转换
  transformIf,            // v-if转换
  transformFor,           // v-for转换
  transformOnce,          // v-once转换
  transformOn,            // v-on转换
  transformBind,          // v-bind转换
  transformModel,         // v-model转换
  transformFilter,        // 过滤器转换
  transformExpression,    // 表达式转换
  transformStatic,        // 静态节点转换
  transformCloak,         // v-cloak转换
  transformPre,           // v-pre转换
  transformMemo,          // v-memo转换
  transformScopeId,       // scopeId转换
  transformFragment,      // Fragment转换
  transformComponent,     // 组件转换
  transformDirective,     // 自定义指令转换
];

Generate:生成 render 函数

从 AST 到代码

Generate 阶段将转换后的 JavaScript AST 生成可执行的 JavaScript 代码,即代码生成:

javascript 复制代码
/**
 * 简单的代码生成器
 */
class CodeGenerator {
  constructor(ast) {
    this.ast = ast;
    this.code = '';
    this.indentLevel = 0;
  }
  
  /**
   * 生成代码
   */
  generate() {
    this.code = `
import { openBlock, createBlock, createVNode } from 'vue'

export function render() {
  return ${this.genNode(this.ast)}
}
    `;
    return this.code;
  }
  
  /**
   * 生成节点代码
   */
  genNode(node) {
    if (!node) return 'null';
    
    switch (node.type) {
      case 'Root':
        return this.genRoot(node);
      case 'Element':
        return this.genElement(node);
      case 'Text':
        return this.genText(node);
      case 'Interpolation':
        return this.genInterpolation(node);
      case 'Conditional':
        return this.genConditional(node);
      default:
        return 'null';
    }
  }
  
  /**
   * 生成根节点
   */
  genRoot(node) {
    if (node.children.length === 1) {
      return this.genNode(node.children[0]);
    } else {
      // 多个根节点,用Fragment包裹
      return `createBlock(Fragment, null, [
        ${node.children.map(child => this.genNode(child)).join(',\n        ')}
      ])`;
    }
  }
  
  /**
   * 生成元素节点
   */
  genElement(node) {
    const tag = JSON.stringify(node.tag);
    const props = this.genProps(node.props);
    const children = this.genChildren(node.children);
    
    if (node.isStatic) {
      // 静态节点提升
      return `createVNode(${tag}, ${props}, ${children}, -1)`;
    } else {
      return `createVNode(${tag}, ${props}, ${children})`;
    }
  }
  
  /**
   * 生成属性
   */
  genProps(props) {
    if (!props || props.length === 0) return 'null';
    
    const propObj = {};
    for (const prop of props) {
      if (prop.name === 'class') {
        propObj.class = prop.value;
      } else if (prop.name.startsWith('@')) {
        propObj['on' + prop.name.slice(1)] = `ctx.${prop.value}`;
      } else {
        propObj[prop.name] = prop.value;
      }
    }
    
    return JSON.stringify(propObj);
  }
  
  /**
   * 生成子节点
   */
  genChildren(children) {
    if (!children || children.length === 0) return 'null';
    
    if (children.length === 1) {
      return this.genNode(children[0]);
    }
    
    return `[
      ${children.map(child => this.genNode(child)).join(',\n      ')}
    ]`;
  }
  
  /**
   * 生成文本节点
   */
  genText(node) {
    return JSON.stringify(node.content);
  }
  
  /**
   * 生成插值表达式
   */
  genInterpolation(node) {
    return `ctx.${node.content}`;
  }
  
  /**
   * 生成条件节点
   */
  genConditional(node) {
    return `${node.condition} ? ${this.genNode(node.consequent)} : null`;
  }
  
  /**
   * 添加缩进
   */
  indent() {
    this.indentLevel++;
  }
  
  /**
   * 减少缩进
   */
  dedent() {
    this.indentLevel--;
  }
  
  /**
   * 写入代码
   */
  write(str) {
    this.code += '  '.repeat(this.indentLevel) + str;
  }
}

完整的编译流程

javascript 复制代码
/**
 * 完整的编译器
 */
class VueCompiler {
  compile(template) {
    // 1. Parse阶段
    const parser = new TemplateParser(template);
    const ast = parser.parse();
    console.log('AST:', JSON.stringify(ast, null, 2));
    
    // 2. Transform阶段
    const transformer = new Transformer(ast);
    transformer
      .use(transformInterpolation)
      .use(transformVIf)
      .use(transformStatic);
    const transformedAst = transformer.transform();
    
    // 3. Generate阶段
    const generator = new CodeGenerator(transformedAst);
    const code = generator.generate();
    
    return code;
  }
}

编译优化详解

静态提升

静态提升是 Vue3 最重要的优化之一,这在我之前的多篇文章中都有提及,本文不再赘述。

Patch Flags

通过标记动态内容,减少比较范围:

javascript 复制代码
// 编译前
<div :class="cls" :id="id">内容</div>

// 编译后
createVNode('div', {
  class: ctx.cls,
  id: ctx.id
}, '内容', 8 /* PROPS */, ['class', 'id'])

Block Tree

收集动态节点,跳过静态子树:

javascript 复制代码
// 编译前
<div>
  <span>静态</span>
  <p :class="dynamic">动态</p>
  <div>
    <span>静态</span>
    <span>{{ text }}</span>
  </div>
</div>

// 编译后
const _hoisted_1 = createVNode('span', null, '静态', -1)
const _hoisted_2 = createVNode('span', null, '静态', -1)

export function render(ctx) {
  return (openBlock(), createBlock('div', null, [
    _hoisted_1,
    createVNode('p', { class: ctx.dynamic }, null, 2 /* CLASS */),
    createVNode('div', null, [
      _hoisted_2,
      createVNode('span', null, ctx.text, 1 /* TEXT */)
    ])
  ]))
}

结语

理解编译三阶段,就像掌握了 Vue 的"翻译官",它把人类友好的模板,翻译成机器高效的代码。这不仅帮助我们写出更高效的 Vue 应用,也为深入理解 Vue 的优化策略打下基础。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

相关推荐
Dream-Y.ocean39 分钟前
鸿蒙PC平台宅匠 3D 室内设计适配实战:基于 Electron 壳方案的 Three.js 双渲染引擎与参数化户型编辑器开发
javascript·3d·harmonyos
Csvn2 小时前
性能监控
前端
Highcharts.js2 小时前
Highcharts Gantt使用符号的自定义数据标签开发甘特图示例
javascript·甘特图·highcharts·图表开发·实战代码·响应式图表
Csvn2 小时前
构建工具
前端
FRing033 小时前
vue2干货总结3(computed计算属性、watch监听器)
开发语言·前端·vue.js·vue2
Java小卷3 小时前
JuanForm vs FormCreate!Vue3 表单引擎联动配置 PK!
vue.js·低代码
kyriewen5 小时前
Skills 榜前 10 我全装了:只有 3 个能直接用
前端·javascript·程序员
IT_陈寒5 小时前
当Java的hashCode遇上Lombok,我的缓存被全清了
前端·人工智能·后端
工控丫丫5 小时前
帆软看板中经常使用的公式函数及经常搭的组合
前端·javascript·数据库
jerrywus6 小时前
我用 GPT-6.1-sol 做了台会讲解的 3D 售货机:60 罐饮料,一口没喝上
前端·agent·claude