编译器三步走:解析 → 转换 → 生成

本节目标

无论 Babel、TypeScript、ESLint 还是 webpack,处理代码的本质都是同一条流水线(Babel 官方的三步):

源代码 ──① 解析 Parse──▶ AST ──② 转换 Transform──▶ 新 AST ──③ 生成 Generate──▶ 新代码
          (读成树)               (在树上改)              (树画回代码)

动手:一条迷你流水线

下面零依赖实现一个超简化版: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 都重要。