编译器

如果说渲染器解决的是“怎么更新页面”,那编译器解决的是另一个问题:

你写的模板,Vue 到底怎么把它变成可以执行的渲染函数?

很多人知道 Vue 可以写:

<div>{{ count }}</div>

但框架真正运行时,浏览器并不认识这种模板语法。

Vue 必须先把它编译成 JavaScript。

先不要急着记细节,只要先记住这三个阶段:parsetransformgenerate,编译器的主线就清楚了。

一、编译器到底在做什么

模板编译主流程可以非常简单地概括成三步:

  1. parse:把模板字符串解析成模板 AST
  2. transform:把模板 AST 转成更适合生成代码的结构
  3. generate:生成渲染函数代码

伪代码:

const ast = parse(template)
transform(ast)
const code = generate(ast)

你可以把它理解成:

  • 先看懂模板
  • 再整理模板
  • 最后输出代码

二、为什么要有 AST

因为字符串不方便直接做结构化分析。

比如模板:

<div>
    <span>{{ title }}</span>
</div>

对于人来说很好理解,但对编译器来说,需要先变成“树形结构”。

例如可以粗略理解成:

{
    type: 'Element',
    tag: 'div',
    children: [
        {
            type: 'Element',
            tag: 'span',
            children: [
                {
                    type: 'Interpolation',
                    content: 'title'
                }
            ]
        }
    ]
}

这就是 AST 的意义:把“文本”变成“结构”。

三、parse 阶段在做什么

parse 的任务,就是把模板字符串拆解成 AST。

这一阶段主要关心:

  • 标签开始和结束
  • 文本内容
  • 插值表达式
  • 属性和指令

比如:

<div id="app">{{ msg }}</div>

编译器至少要看出几件事:

  • 有一个 div
  • 它有个 id 属性
  • 它的子节点里有一段插值表达式

四、不要把 parse 想得太神秘

从思路上讲,parse 就是在不断读取字符串,然后判断当前读到的是:

  • 开始标签
  • 结束标签
  • 文本
  • 插值

一个极简思路可以理解成:

function parse(template) {
    while (template not end) {
        if (startsWithTagOpen()) {
            parseElement()
        } else if (startsWithInterpolation()) {
            parseInterpolation()
        } else {
            parseText()
        }
    }
}

真正源码会复杂很多,但大方向就是“边读边识别结构”。

五、transform 阶段为什么存在

很多人学编译器时容易问:

既然已经有 AST 了,为什么不能直接生成代码?

可以,但不划算。

因为模板 AST 更像“忠实还原模板结构”,而生成代码时,我们更关心的是:

  • 这个节点最后应该生成什么运行时代码
  • 哪些节点是静态的
  • 哪些节点需要打标记
  • 哪些结构可以优化

所以 transform 的作用,就是把 AST 变成更适合代码生成的形态。

六、transform 阶段会做哪些重要事情

1. 遍历 AST

它会递归访问每个节点。

2. 根据节点类型做转换

例如:

  • 元素节点怎么处理
  • 文本节点怎么处理
  • 插值节点怎么处理
  • 指令节点怎么处理

3. 注入运行时需要的信息

比如把模板里的节点转换成后续生成 createVNodetoDisplayString 等调用所需的信息。

4. 进行编译优化

例如:

  • 静态提升
  • Patch Flag 标记
  • Block Tree 相关优化

七、为什么编译优化这么重要

因为 Vue 不只是想把模板“翻译成能运行的代码”,还想让更新更高效。

比如下面这个模板:

<div>
    <p>static text</p>
    <span>{{ count }}</span>
</div>

编译器其实能提前看出来:

  • p 是纯静态节点
  • span 才是动态节点

这样渲染器更新时就不需要每次都把整棵子树当成同样重要的内容处理。

八、generate 阶段在做什么

这一阶段负责把处理后的 AST 输出成 JavaScript 代码。

最终产物通常会是一个渲染函数。

比如模板:

<div>{{ msg }}</div>

最终生成的代码思路可能接近:

return function render(_ctx) {
    return h('div', null, _ctx.msg)
}

真实 Vue 生成的代码会更复杂,但核心目标一致:

  • 让模板最终变成函数
  • 让运行时可以直接执行这个函数得到 VNode

九、从模板到页面,完整链路是什么

把整条链路串起来,其实就是:

  1. 模板编译成 render 函数
  2. render 函数执行后得到 VNode
  3. 渲染器根据 VNode 挂载/更新页面

也就是说:

编译器负责把“模板”变成“可执行描述”,渲染器负责把“描述”变成“页面”。

十、源码层面最值得抓的几个点

如果你准备读 Vue 编译器源码,建议先抓下面几条主线:

1. baseParse

负责把模板解析成 AST。

2. transform

负责遍历 AST,并调用各种节点转换逻辑。

3. generate

负责拼接最终代码字符串。

4. 节点转换函数

例如:

  • 元素转换
  • 文本转换
  • 插值转换
  • 指令转换

十一、一个简化版源码脉络

你可以把编译器主线理解成这样:

function compile(template) {
    const ast = baseParse(template)
    transform(ast, {
        nodeTransforms: [
            transformElement,
            transformText,
            transformExpression,
        ],
    })
    return generate(ast)
}

真实源码当然远不止这些,但这段伪代码足够帮你建立整体结构。

十二、为什么说 Vue 的性能优化很多发生在编译阶段

因为编译器是在“模板还没运行前”就能看到全局结构的。

它能提前知道:

  • 哪些节点是静态的
  • 哪些属性会变
  • 哪些节点需要重点追踪

这使得很多优化可以在编译时就提前决定,而不是都放到运行时处理。

这也是 Vue 3 编译器价值非常大的地方。

十三、学编译器时最容易卡住的点

1. 一开始就陷进 tokenizer 细节

当然可以看,但别一开始就被字符状态机困住。

先理解三阶段主流程更重要:

  • parse
  • transform
  • generate

2. 把模板 AST 和 JavaScript AST 混在一起

对初学者来说,不必过早执着于“每一层 AST 的精确结构”,先理解“模板结构会逐步转成代码结构”就够了。

3. 不理解编译器和运行时的边界

编译器不负责真的去更新页面,它负责生成更适合运行时执行的代码。

总结

编译器最核心的作用是:

  • 把模板字符串解析成结构化 AST
  • 把 AST 转换成更适合运行时代码生成的形态
  • 输出 render 函数,让渲染器后续去执行

学 Vue 编译器,最重要的不是背细节,而是先抓住这条主线:

  • 模板 -> AST -> render 函数
  • render 函数 -> VNode
  • VNode -> 页面
上次更新:
贡献者: Joe, joe