编译器三步走:解析 → 转换 → 生成
本节目标
- 理解 AST 在“编译器/转译器”流水线里的位置
- 用一个 30 行的迷你例子,跑通 解析→转换→生成 全过程
- 建立核心心智模型:转换(Transformation)才是你写插件时真正动手的地方
无论 Babel、TypeScript、ESLint 还是 webpack,处理代码的本质都是同一条流水线(Babel 官方的三步):
源代码 ──① 解析 Parse──▶ AST ──② 转换 Transform──▶ 新 AST ──③ 生成 Generate──▶ 新代码
(读成树) (在树上改) (树画回代码)- ① 解析 Parse:把字符串变成 AST。它内部又分两步——词法分析(切 token)和语法分析(token 拼成树)。(这两步我们第 3、4 节亲手写一遍。)
- ② 转换 Transform:遍历 AST,增删改节点。这是 Babel 插件、ESLint 规则、代码压缩器真正干活的地方。
- ③ 生成 Generate:把改好的 AST 再“画”回字符串代码,并生成 source map。
动手:一条迷你流水线
下面零依赖实现一个超简化版:parse(硬编码成 AST)→ transform(把变量名 answer 改成 result)→ generate(把 AST 拼回代码)。
// 运行环境:Node.js 18+(无需任何依赖)
// 运行方式:把整段保存为 app-l2.cjs,终端执行 node app-l2.cjs
// ① 解析:真实场景由解析器生成,这里直接给出 AST(对应 `const answer = 1 + 2`)
function parse() {
return {
type: 'VariableDeclaration', kind: 'const',
declarations: [{
type: 'VariableDeclarator',
id: { type: 'Identifier', name: 'answer' },
init: { type: 'BinaryExpression', operator: '+',
left: { type: 'Literal', value: 1 }, right: { type: 'Literal', value: 2 } }
}]
}
}
// ② 转换:遍历 AST,把所有名为 answer 的标识符改成 result
function transform(ast) {
function walk(node) {
if (!node || typeof node !== 'object') return
if (node.type === 'Identifier' && node.name === 'answer') node.name = 'result'
for (const key of Object.keys(node)) {
const val = node[key]
if (Array.isArray(val)) val.forEach(walk)
else if (val && typeof val === 'object') walk(val)
}
}
walk(ast)
return ast
}
// ③ 生成:把 AST 重新拼成代码字符串
function generate(ast) {
const d = ast.declarations[0]
const id = d.id.name
const init = d.init
const expr = init.left.value + ' ' + init.operator + ' ' + init.right.value
return ast.kind + ' ' + id + ' = ' + expr
}
// 跑通流水线
const ast1 = parse()
const ast2 = transform(ast1)
const code = generate(ast2)
console.log('转换后代码:', code) // 转换后代码: const result = 1 + 2
// === 调用示例说明 ===
// parse() -> 返回原始 AST(answer)
// transform(ast) -> 原地修改 AST,把 answer 改名 result,返回同一棵树
// generate(ast) -> 把树渲染成代码字符串名词解释
流水线(Pipeline):一串“按顺序处理数据”的步骤,上一步的输出是下一步的输入。淘米的流程(洗 → 煮 → 盛)就是流水线。代码处理工具的通用骨架是:输入源码 → 输出新源码,中间必经“树”。
转换(Transform):流水线的第 ② 步,在一棵已有的 AST 上做增删改。它是你写 Babel 插件、ESLint 规则、代码压缩器时真正动手的地方——第 ① 和第 ③ 步只是把“文字”和“树”互相搬运,真正改变代码含义的是第 ② 步。
课后练习
练习 1:用一句话给 Babel、ESLint、webpack 这三种工具归类它们共同的工作步骤。
答案:它们都遵循同一套三步流水线——
Parse(源码→AST)→ Transform(在树上改)→ Generate(AST→新源码)。区别只在于第 ② 步“改什么”:Babel 改语法、ESLint 报问题、webpack 做打包优化。
练习 2:在 app-l2 的迷你例子里,如果把 transform 改成“把所有数字字面量的值 +1”,那么 generate 最终输出的代码会变成什么?
答案:
const result = 2 + 3。原因:原 AST 中1和2两个Literal节点的值分别被 +1 变成2和3,generate再把这棵改好的树拼回代码字符串。
本节小结(观点与完整描述)
学完本节,你得到的不是三个英文单词,而是一副“看穿所有代码工具”的眼镜:以后再看到任何处理代码的库,你都会下意识去问“它的 AST 在哪一步被改了”。整条流水线里,真正动手改树的是第 ② 步 Transform,第 ① 和第 ③ 步只是把文字和树互相搬运、并不改变代码含义。掌握了这个骨架,你写 Babel 插件或 ESLint 规则时就不会再“不知道从哪下手”——你只是在第 ② 步的树上做修改而已。记住它,比记住任何具体 API 都重要。