Babel的编译流程

客子游他乡 / 2024-02-20 / 原文

Babel的编译流程:指的是将使用较新ECMAScript标准(如ES6、ES7、ES8等)编写的JavaScript代码转换为向下兼容的JavaScript版本的过程。这个过程分为三个步骤:

  • 解析(Parse):首先,Babel使用解析器(parser)将源代码(source code)转换成抽象语法树(Abstract Syntax Tree,AST)。抽象语法树是一种数据结构,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构;

  • 转换(Transform):在解析阶段生成了AST之后,Babel进入转换阶段。在这个阶段,Babel会遍历AST,并使用一系列的转换插件对AST的节点进行增删改等操作。这些转换插件可能包括将新语法转换为旧语法、添加polyfill以实现新特性、优化代码等(开发者编写的插件绝大多数是在这阶段使用)。

  • 生成(Generate):最后,Babel将转换后的AST转换回代码,并生成源代码映射(Source Map)以便于调试。生成的代码是向下兼容的,可以在旧版本的浏览器或环境中运行。

babel parse process
Babel流程解析图-1

这三步都做了什么?

  • parse

    • Babel 的 parse 阶段是编译流程中的第一步,主要任务是将输入的 JavaScript 代码(源代码)转换成抽象语法树(Abstract Syntax Tree,AST)。这个过程可以分为两个主要步骤:词法分析和语法分析
    • 词法分析(Lexical Analysis):词法分析器(也称为扫描器或词法器)读取源代码,将其分解成一系列的词法单元(tokens)。这些词法单元是编程语言的基本构成元素,如变量名、关键字(如 functionif 等)、操作符(如 +- 等)、数字、字符串等。词法分析器会去除源代码中的空白符、注释等无关紧要的元素,并将源代码转换为 token 序列。
    • 语法分析(Syntax Analysis):在词法分析的基础上,语法分析器根据语言的语法规则,将 token 序列组合成抽象语法树(AST)。AST 是一个树形结构,它反映了代码的语法结构。每个节点都代表源代码中的一种结构,如函数声明、变量赋值、条件语句等。语法分析器会根据语言的定义来验证源代码的语法正确性,并生成相应的 AST(通过不同的对象来保存不同的数据,并且按照依赖关系组织起来,这种数据结构就是抽象语法树(abstract syntax tree),为开发者提供了方便的方式来分析和操作源代码,如代码重构、静态分析、代码生成等)。我们可以通过AST可视化查看工具来直观的查看AST的数据结构,例如下图
ast可视化图-2

不过在最新版本的Babel中放弃了自己的词法分析器,转而使用了@babel/parser,这是因为在7.0这个版本中,Babel 进行了大量的重构和模块化工作,将原本的单体库拆分为多个小型的、可独立安装的 npm 包。@babel/parser 就是其中之一,它负责将 JavaScript 代码解析为抽象语法树(AST)(上图的下拉栏可以看到多种不同parser工具)。

  • transform

    • 在解析阶段生成了AST之后,会进入转换阶段。在这个阶段,Babel会遍历AST,并使用一系列的转换插件对AST的节点进行增删改等操作。这些转换插件可能包括将新语法转换为旧语法、添加polyfill以实现新特性、优化代码等。当我们在Babel的配置文件中注册了转换插件(Transform Plugins),在transform阶段就会被调用l来,兼容性问题就是在这个阶段处理的。例如,如果代码中使用了某些新的 API 或特性,而目标环境不支持这些特性,Babel 会自动引入相应的 polyfill 来填补这些差异。经过上述的步骤后,原始的 AST 会被转换成一个新的 AST,这个新的 AST 代表了经过转换后的代码结构。这个新的 AST 随后会被传递给 generate 阶段,以生成最终的代码输出。

    • Babel提供了@babel/traverse的traverse方法来便利和修改AST,traverse方法常用的就两个两个参数:遍历的ast和制定的visitor函数(visitor 是指定对什么 AST 做什么处理的函数,可以指定刚开始遍历(enter)和遍历结束后(exit)两个阶段的回调函数);
    • traverse(ast, {
        FunctionDeclaration: {    // 遇到声明函数时
            enter(path, state) {}, // 进入节点时调用
            exit(path, state) {} // 离开节点时调用
        }
      })
      
      //!=====================
      
      traverse(ast, {
        FunctionDeclaration(path, state) {} // 进入节点时调用
      })