从生活实例解释什么是AST抽象语法树

122 阅读10分钟

AST(Abstract Syntax Tree,抽象语法树)  听起来很高深,但其实它的核心概念非常简单:把“文本”变成“结构化的数据对象” ,方便机器理解和操作。就是把字符串形式的代码转换成机器能看懂、能操作的结构化数据—— 你可以把它理解成:代码的 “说明书”/“骨架”

机器(比如 Babel、Vue 编译器)看不懂直接的字符串代码(比如const a = 1),但能看懂 AST 这种 “键值对 + 层级结构” 的 JSON-like 数据,从而实现「修改代码、转换代码、分析代码」。

为了让你彻底明白,我们分两步走:先看生活中的例子,再看 Vue 中的实际应用。


第一部分:生活中的例子 —— “点外卖”

假设你是个复杂的客户,你给服务员说了一句很长的话(这就是源代码 Source Code):

“我要一个牛肉汉堡,不要洋葱,加双份芝士,还要一杯可乐,去冰。”

1. 为什么需要 AST?

如果你直接把这句话扔给后厨的厨师,厨师可能听懵了,或者容易漏掉细节。计算机也是一样,它看不懂这一长串字符串,它需要一个清晰的清单

2. 生成 AST(解析过程)

前台服务员(编译器/解析器)听到这句话后,会在点餐系统里输入一张结构化的单子。这张单子就是 AST

它大概长这样:

{
  "类型": "订单",
  "内容": [
    {
      "商品": "牛肉汉堡",
      "配料修改": [
        { "操作": "移除", "物品": "洋葱" },
        { "操作": "添加", "物品": "芝士", "数量": 2 }
      ]
    },
    {
      "商品": "可乐",
      "属性": [
        { "温度": "去冰" }
      ]
    }
  ]
}

3. 这个例子的核心点:

  • 源代码:那句口语(字符串)。
  • AST:那张结构化的单子(JSON 对象)。
  • 作用:有了这张单子,厨师(浏览器/JS引擎)不需要去分析语法,直接看字段就能精准干活;甚至如果需要把“汉堡”换成“三明治”,改单子(修改 AST)比改口语容易得多。

二、回到代码:AST 到底解决了什么问题?

场景:你写了一行代码 const msg = 'hello',想把它改成 var message = 'hello'

  • 如果你直接改字符串:需要 “找 const→替换成 var,找 msg→替换成 message”,但代码复杂时(比如嵌套函数、多文件),手动 / 字符串替换极易出错;
  • 用 AST 改:机器先把代码转成 AST(结构化数据),再精准修改节点,最后转回代码 —— 安全、精准、可批量操作。

第一步:解析(Parse)—— 代码→AST

const msg = 'hello' 对应的 AST 简化结构:

{
  "type": "VariableDeclaration", // 节点类型:变量声明
  "kind": "const", // 变量类型:const
  "declarations": [
    {
      "type": "VariableDeclarator",
      "id": { "type": "Identifier", "name": "msg" }, // 变量名:msg
      "init": { "type": "Literal", "value": "hello" } // 变量值:hello
    }
  ]
}

此时代码不再是字符串,而是 “变量声明节点 + 变量名节点 + 值节点” 的结构化数据,每个部分都有明确标识。

第二步:转换(Transform)—— 修改 AST

机器遍历 AST,精准修改指定节点。比如我们想把const改成varmsg改成message

// 伪代码:修改 AST 节点 
ast.kind = "var"; // 把const换成var
ast.declarations[0].id.name = "message"; // 把msg换成message

修改后的 AST

{
  "type": "VariableDeclaration",
  "kind": "var", // 已修改
  "declarations": [
    {
      "type": "VariableDeclarator",
      "id": { "type": "Identifier", "name": "message" }, // 已修改
      "init": { "type": "Literal", "value": "hello" }
    }
  ]
}

第三步:生成(Generate)——AST→代码

把修改后的 AST 转回字符串代码,核心是 “遍历 AST 树,根据节点类型拼接代码”。我们可以写一个极简的生成函数模拟这个过程:

// 迷你AST生成器:遍历节点拼接代码
function generateCode(astNode) {
  // 处理变量声明节点
  if (astNode.type === "VariableDeclaration") {
    const declarations = astNode.declarations.map(decl => {
      const name = decl.id.name;
      const value = decl.init.value;
      return `${name} = '${value}'`;
    }).join(', ');
    return `${astNode.kind} ${declarations};`;
  }
}

// 执行生成
const newCode = generateCode(ast);
console.log(newCode); // 输出:var message = 'hello';

修改后的 AST 转回字符串代码:var message = 'hello'

真实场景中,Babel、Vue 编译器会用更完善的生成器(如@babel/generator),但核心逻辑都是 “节点类型→代码片段→拼接”。

Vue 中的 AST

在 Vue 中,AST 主要用于模板编译(Template Compilation)

浏览器其实只认识 HTML、CSS 和 JS。它根本不认识 Vue 的 .vue 文件,也不认识 v-if、v-for这种语法。

Vue 需要把你的 `` 变成浏览器能运行的 render 函数,中间的桥梁就是 AST

  1. 源代码(你写的 Vue 模板)
<div id="app">
  <p>你好</p>
</div>

这就好比刚才那句“我要一个汉堡...”,对浏览器来说,这只是一串普通的字符串。

2. 解析成的 AST(Vue 内部生成的树)

Vue 的编译器会把上面的 HTML 字符串“拆解”,变成下面这样的 JavaScript 对象(简化版):

const ast = {
  // 标签类型
  tag: &#34;div&#34;,
  // 属性列表
  attrs: [{ name: &#34;id&#34;, value: &#34;app&#34; }],
  // 子节点列表
  children: [
    {
      tag: &#34;p&#34;,
      // 指令被解析成了专门的属性
      if: &#34;show&#34;, 
      children: [
        {
          type: &#34;text&#34;,
          text: &#34;你好&#34;
        }
      ]
    }
  ]
};

3. 为什么要转成 AST?(Vue 拿它干什么?)

一旦变成了上面这种树形对象,Vue 就可以对代码进行**“手术”“优化”**:

  1. 识别指令:Vue 扫描这棵树,发现 p 节点有个 if: "show"。于是它知道:生成代码时,要给这行代码加个 if (show) { ... } 的判断逻辑。
  2. 静态提升(优化性能) :Vue 3 扫描 AST,发现 "你好" 是纯文本,永远不会变。Vue 就会给它打个标记:“这块不需要每次渲染都比较,直接复用”。(如果只是看字符串,很难做这种复杂的分析)。

AST 的下一步,是生成 render 函数代码(渲染函数)。

要搞懂 AST 如何转回字符串代码,核心是理解「AST 生成器(Generator)」的工作逻辑 —— 它本质是深度遍历 AST 树,根据每个节点的类型和属性,拼接出对应的代码字符串

第一阶段:AST ➡️ Render 函数代码

这就是浏览器能“认识”的第一步:因为它变成了标准的 JavaScript 代码。

浏览器虽然不懂 <p>,但它懂 JavaScript 的 if 或者三元运算符 ? :。

举个栗子

你的 Vue 模板(源代码):

<div id="app">
  <p>你好</p>
</div>

生成的 AST(中间产物,略):
(就是一个描述结构的 JSON 对象)

AST 转换后生成的 Render 函数代码(最终产物):

Vue 的编译器会根据 AST,拼接出一段 纯 JavaScript 字符串,长得像这样(为了方便阅读,我简化了 Vue 内部的函数名):

function render() {
  // _c = createElement (创建元素)
  // _v = createTextVNode (创建文本)
  // _e = createEmptyVNode (创建空节点,用于 v-if 为 false 时)

  return _c('div', { attrs: { &#34;id&#34;: &#34;app&#34; } }, [
    // 重点看这里!v-if 被变成了 JS 的三元运算符
    (show) 
      ? _c('p', [_v(&#34;你好&#34;)]) 
      : _e()
  ])
}

这里的核心变化:

  1. HTML 标签 变成了函数调用 _c('div')。
  2. v-if="show"  消失了,变成了原生的 JS 逻辑 (show) ? ... : ...。
  3. 浏览器完全认识这段代码!  这就是一段标准的 JS 函数,里面全是函数调用和逻辑判断。

第二阶段:浏览器怎么把这段代码变成画面?

你可能会问:“浏览器运行了这个函数,然后呢?屏幕上怎么就有字了?”

这里有两个步骤:生成虚拟 DOM ➡️ 转为真实 DOM

1. 运行 Render 函数,得到 虚拟 DOM (Virtual DOM)

当 Vue 运行时(Runtime)执行上面的 render 函数时,浏览器并不会立即去画界面,而是返回一个 JS 对象树,这叫做 VNode(虚拟节点)

执行 render() 后得到的返回值:

// 这是一个纯 JS 对象,不是真实的 DOM 元素
{
  tag: 'div',
  data: { attrs: { id: 'app' } },
  children: [
    {
      tag: 'p',
      children: [{ text: '你好' }]
    }
  ]
}

为什么要多这一步?
因为操作真实 DOM(网页上的元素)非常慢,而操作 JS 对象非常快。Vue 可以在这个 JS 对象上做各种计算(比如 Diff 算法),确认没问题了,再动手改网页。

2. Patch(修补/渲染)➡️ 真实 DOM

这是最后一步。Vue 的运行时系统(Runtime)会拿着上面的 VNode,调用浏览器底层的 DOM API

这时候,浏览器才真正干活:

  • Vue 看到 tag: 'div' ➡️ 调用 document.createElement('div')
  • Vue 看到 attrs: { id: 'app' } ➡️ 调用 el.setAttribute('id', 'app')
  • Vue 看到 text: '你好' ➡️ 调用 document.createTextNode('你好')
  • 最后把它们拼在一起,挂载到页面上。

总结

  • AST 是什么?
    它是代码的骨架图。它把代码从“一行行文本”变成了“层级分明的对象”。
  • Vue 里的流程:
    template (字符串) ➡️ AST (树形对象)  ➡️ render 函数 (可执行 JS) ➡️ 虚拟 DOM ➡️ 真实 DOM。

vite创建的vue项目是通过babel还是vue自己编译器编译的

在默认的 Vite + Vue 项目中,绝大多数情况下,是不需要 Babel 的,也没有用 Babel。 它的分工是这样的:

  1. .vue 文件的编译(Template -> Render函数) :完全依靠 Vue 自己的编译器(@vue/compiler-sfc)。
  2. JS/TS 语法的转译(ES6+ -> 浏览器能跑的代码) :主要依靠 Esbuild(一个用 Go 语言写的、速度极快的构建工具)。

详细拆解:谁在干活?

为了搞清楚这个问题,我们需要把你写代码时的两个“转换”动作分开看:

1. 动作一:把 Vue 模板变成 JS 代码

也就是刚才我们聊的:v-if -> render 函数。

  • 负责工头Vue Compiler (@vue/compiler-sfc)

  • 工具链:Vite 里的插件 @vitejs/plugin-vue 会调用这个 Vue 编译器。

  • AST 产生地:这里产生的 AST 是 Vue 专有的 Template AST

  • 结论:这块跟 Babel 毫无关系。哪怕你安装了 Babel,Vue 模板编译也不归 Babel 管。

    2. 动作二:把高级 JS/TS 变成浏览器能懂的 JS

比如你用了箭头函数 () => {},或者 TypeScript 的类型标注 name: string,或者最新的 ?. 语法。

  • 传统做法(Webpack 时代)
    这是 Babel 的地盘。Webpack 会用 babel-loader 把这些新语法转成老旧的 ES5 代码,为了兼容 IE 等老浏览器。

  • 现代做法(Vite 时代)
    Vite 默认认为你不需要兼容 IE(除非你专门配置)。现代浏览器(Chrome, Edge, Firefox, Safari)都已经支持 ES6 模块了。

    • 开发环境 (npm run dev)
      Vite 使用 Esbuild 来处理 JS 和 TS。
      Esbuild 比 Babel 快 10-100 倍。因为它不需要把代码转成复杂的 ES5,只需要把 TypeScript 的类型去掉,把极少数浏览器不支持的语法微调一下即可。

    • 生产打包 (npm run build)
      Vite 使用 Rollup 进行打包,同时默认使用 Esbuild 进行代码压缩和转换。

      什么时候 Vite 才会用到 Babel?

虽然 Vite 默认不用 Babel,但在一种情况下它会把 Babel 请回来:

你需要兼容“老古董”浏览器时(比如 IE11 或旧版 Chrome)。 如果你安装了 @vitejs/plugin-legacy 插件:

// vite.config.js
import legacy from '@vitejs/plugin-legacy'

export default {
  plugins: [
    vue(),
    legacy({
      targets: ['ie >= 11'], // 只要你需要支持这些老家伙
      additionalLegacyPolyfills: ['regenerator-runtime/runtime']
    })
  ]
}

这时候,Vite 就会在打包时自动下载并使用 Babel,把你的现代代码狠狠地转译成 ES5,以确保在老浏览器上不报错。

总结对照表

任务Webpack (Vue CLI)Vite (现代模式)
解析 .vue 模板Vue Loader (调用 Vue Compiler)@vitejs/plugin-vue (调用 Vue Compiler)
JS 转译 (ES6->ES5)Babel (必装,很慢)Esbuild (内置,极快,不转 ES5)
TS 转译Babel 或 ts-loaderEsbuild (毫秒级完成)
AST 类型Vue AST + Babel ASTVue AST + Esbuild AST

Vite 项目里:

  1. Vue 编译器 负责把  里的代码变成 render 函数(利用 Vue AST)。

  2. Esbuild 负责把你的 JS/TS 变成浏览器能运行的 JS(不做过度的向下兼容)。

  3. Babel 默认是不存在的,除非你为了兼容性专门请它出山。

    所以,Vite 快的原因之一,就是把“慢吞吞”的 Babel 给优化掉了!


结论

转换成 AST 之后的代码,就是 render 函数(JavaScript 代码)。

怎么让浏览器认识?
因为那已经是纯粹的 JavaScript 了!浏览器执行这段 JS,生成虚拟节点对象,最后 Vue 内部通过 document.createElement 等原生 API 把这些对象变成了屏幕上的像素。