编译器

2025/3/13 Vue3编译

# 编译流程

编译:把 A (源代码)翻译为 B (目标代码)的过程

源代码 => 词法分析 => 语法分析 => 语义分析 => 中间代码生成 => 优化 => 目标代码生成 => 目标代码

编译前端:词法分析、语法分析、语义分析,通常与目标的平台无关,仅负责分析源代码

编译中端:中间代码生成、优化

编译后端:目标代码生成,通常与目标的平台有关,负责生成中间代码和目标代码,可能会包含编译中端

源代码:

<div>
  <h1 :id="appId">this is h1</h1>
</div>

经过 vue 的模板编译后生成的目标代码:

function render() {
  return h('div', [
    h('h1', { id: appId }, 'this is h1')
  ])
}
<div>
  <h1 v-if="ok">this is h1</h1>
</div>

转化成的 ast 如下

const ast = {
  type: 'root',
  children: [
    {
      type: 'element',
      tag: 'div',
      children: [
        {
          type: 'element',
          tag: 'h1',
          props: [
            {
              // 指令
              type: 'directive',
              // 指令名称
              name: 'if',
              expression: {
                // 表达式
                type: 'expression',
                content: 'ok'
              }
            }
          ],
        }
      ]
    }
  ]
}
  1. 根据 type 来进行区分
  2. 子节点都在 children 中
  3. 属性节点和指定节点存储在 props 中
  4. 不同类型的节点会有不同的对象属性描述

以上可以通过下面的代码来解释

const template = `
<div>
  <h1 v-if="ok">this is h1</h1>
</div>
`
// 模板通过解析生成 templateAst
const templateAst = parse(template)
// 经过转换生成 jsAst
const jsAst = transforme(templateAst)
// 生成渲染函数
const code = generate(jsAst)

流程:

  1. 将模板字符串解析为模板 AST ,解析器 parser
  2. 将模板 AST 转换为 jsAST, 转换器 transformer
  3. 根据 jsAST 生成渲染函数,生成器 generator

该流程也可以参考 base/compiler/mini-compiler.js

# parse 实现

parse

# 构造 ast

由于 html 的标签格式比较固定,标签元素之间天然嵌套,形成父子关系,因此可以通过树形结构构造一个 ast

构造的过程中我们可以维护一个栈 elementStack ,在扫描 tokens 列表的过程中,每遇到开始一个节点我们就把它压入栈中,遇到一个结束标签则把当前栈顶的节点弹出。

# ast 的转换

当我们可以访问每个 ast 节点的时候,那就可以对某些特定节点进行增删改操作。

transform

为了解耦节点的访问和操作,使用了插件化的架构,将节点的操作封装到独立的转换函数中。

transform2

context: 上下文,可以理解为程序在某个范围内的全局变量

  • reactReact.createContext 创建的上下文对象,允许将数据一层一层传递下去,无论组件嵌套多深都可以访问到
  • vue 中的 inject/provide

transform3

在转换的时候,要保证子节点全部处理完成之后,才能处理处理父节点。因此对节点的访问需要增加进入和退出2个阶段。

当函数转换处于进入时,先进入父节点,然后进入子节点,当函数处于退出时,先退出子节点,再退出父节点。

transform4

# ast 转为 jsAST

const template = `<div><p>Vue</p><p>React</p></div>`

等价的渲染函数

function render() {
  return h('div', [
    h('p', 'Vue'),
    h('p', 'React')
  ])
}

transform

通过将模板 ast 转换为 jsAst 之后,可以通过跟节点的 jsNode 来访问转换后的 jsAst

js 中的函数声明语句

const FunctionDeclNode = {
  // 代表该节点是函数声明
  type: 'FunctionDecl',
  // id 函数名称 标识符为 Identifier
  id: {
    type: 'Identifier',
    name: 'render'
  },
  // 函数的参数
  params: [],
  // 函数体,可以包含多个语句
  body: [
    {
      type: 'ReturnStatement',
      return: {
        type: 'CallExpression',
        callee: {
          type: 'Identifier',
          name: 'h'
        },
        arguments: [
          // 第一参数是字符串 div
          {
            type: 'StringLiteral',
            value: 'div'
          },
          // 第二个参数是数组
          {
            type: 'ArrayExpression',
            elements: [
              // 第一个元素 h 调用
              {
                type: 'CallExpression',
                callee: {
                  type: 'Identifier',
                  name: 'h'
                },
                // 第一个参数是字符串 p, 第二个参数也是 字符串
                arguments: [
                  {
                    type: 'StringLiteral',
                    value: 'p'
                  },
                  {
                    type: 'StringLiteral',
                    value: 'Vue'
                  }
                ]
              },
              {
                type: 'CallExpression',
                callee: {
                  type: 'Identifier',
                  name: 'h'
                },
                arguments: [
                  {
                    type: 'StringLiteral',
                    value: 'p'
                  },
                  {
                    type: 'StringLiteral',
                    value: 'React'
                  }
                ]
              }

            ]
          }
        ]
      }
    }
  ]
};

# 代码生成

代码生成的本质是字符串的拼接,为每一种类型的节点生成相符合的 js 代码。

  1. 定义上下文 context
  2. 储存最终生成的渲染函数的 js 代码片段, context.code
  3. 代码拼接方法 context.push
  4. 通过 generateNode 来生成代码
  5. 返回 context.code

generate

Last Updated: 2026/06/09/ 06:29:26