从生活实例解释什么是AST抽象语法树
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改成var,msg改成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。
- 源代码(你写的 Vue 模板)
<div id="app">
<p>你好</p>
</div>
这就好比刚才那句“我要一个汉堡...”,对浏览器来说,这只是一串普通的字符串。
2. 解析成的 AST(Vue 内部生成的树)
Vue 的编译器会把上面的 HTML 字符串“拆解”,变成下面这样的 JavaScript 对象(简化版):
const ast = {
// 标签类型
tag: "div",
// 属性列表
attrs: [{ name: "id", value: "app" }],
// 子节点列表
children: [
{
tag: "p",
// 指令被解析成了专门的属性
if: "show",
children: [
{
type: "text",
text: "你好"
}
]
}
]
};
3. 为什么要转成 AST?(Vue 拿它干什么?)
一旦变成了上面这种树形对象,Vue 就可以对代码进行**“手术”和“优化”**:
- 识别指令:Vue 扫描这棵树,发现 p 节点有个 if: "show"。于是它知道:生成代码时,要给这行代码加个 if (show) { ... } 的判断逻辑。
- 静态提升(优化性能) :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: { "id": "app" } }, [
// 重点看这里!v-if 被变成了 JS 的三元运算符
(show)
? _c('p', [_v("你好")])
: _e()
])
}
这里的核心变化:
- HTML 标签 变成了函数调用 _c('div')。
- v-if="show" 消失了,变成了原生的 JS 逻辑 (show) ? ... : ...。
- 浏览器完全认识这段代码! 这就是一段标准的 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。 它的分工是这样的:
- .vue 文件的编译(Template -> Render函数) :完全依靠 Vue 自己的编译器(@vue/compiler-sfc)。
- 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-loader | Esbuild (毫秒级完成) |
| AST 类型 | Vue AST + Babel AST | Vue AST + Esbuild AST |
Vite 项目里:
-
Vue 编译器 负责把
里的代码变成 render 函数(利用 Vue AST)。 -
Esbuild 负责把你的 JS/TS 变成浏览器能运行的 JS(不做过度的向下兼容)。
-
Babel 默认是不存在的,除非你为了兼容性专门请它出山。
所以,Vite 快的原因之一,就是把“慢吞吞”的 Babel 给优化掉了!
结论
转换成 AST 之后的代码,就是 render 函数(JavaScript 代码)。
怎么让浏览器认识?
因为那已经是纯粹的 JavaScript 了!浏览器执行这段 JS,生成虚拟节点对象,最后 Vue 内部通过 document.createElement 等原生 API 把这些对象变成了屏幕上的像素。