[babel] babel的工作原理

cnblogs 2024-08-01 08:11:00 阅读 79

Babel 是一个多功能的 JavaScript 编译器,主要用于将现代 JavaScript 语法转换为向后兼容的代码。其工作流程包括解析(parse)、转换(transform)和生成(generate)三个步骤,最终生成兼容代码和源码映射。通过自定义插件,开发者可以灵活地操作 AST,实现代码转换。

Babel是什么

<code>Babel 是一个通用的多功能的 JavaScript 编译器。主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

常见的用途有:

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的功能(通过引入第三方 polyfill 模块,例如 core-js)
  • 源码转换(codemods)

例如我们在React经常使用 JSX 语法,由于这不是JS原生语法,所以不能直接被 JS 引擎编译执行。在代码被执行之前,需要使用编译器进行转译,转换成 JS 代码。

image-20240731143915451

Babel的工作原理

从代码到代码的过程,也是从字符串到字符串的过程。当然不可能直接在字符串上进行操作。中间过程生成了抽象语法树(Abstract Syntax Tree,简称AST),用树来表示代码的结构和语义。

<code>Babel的主要处理步骤是:解析(parse)转换(transform)生成(generate)

graph LR

A((CODE)) --> B[Parse]

B --> C((AST))

C --> D[Transform]

D --> E((AST'))

E --> F[Generate]

F --> G((CODE'))

对于不了解编译原理的前端开发人员,这里推荐一个github上面的mini项目:jamiebuilds/the-super-tiny-compiler: ⛄ Possibly the smallest compiler ever (github.com)

这是一个使用JS编写的超级简单但是包含了上述三个主要步骤的编译器。

代码就几百行,加上注释有一千多行,讲解非常详细。适合入门。

解析 parse

“解析”这一过程主要是通过读取代码字符串,构建出 AST 。

主要包含词法分析语法分析这两个阶段。

词法分析

词法分析部分使用tokenizer方法记录一个tokens列表,为代码中的每一个token标注其类型和值:

从源码中可以看到Token除了记录类型和值,还记录了这个词在源代码中的位置,即startendloc等属性。

token是指代码中独立的最小单元,它可以是数字字面量(NumberLiteral)、字符串字面量(StringLiteral)、操作符(operator)等等。

export class Token {

constructor(state: State) {

this.type = state.type;

this.value = state.value;

this.start = state.start;

this.end = state.end;

this.loc = new SourceLocation(state.startLoc, state.endLoc);

}

declare type: TokenType;

declare value: any;

declare start: number;

declare end: number;

declare loc: SourceLocation;

}

在词法分析完成之后,会得到一个tokens数组,记录了代码中每个词的记录。

比如下面这个简单的语句:

n * n;

词法分析之后将得到如下的数组:

[

{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },

{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },

{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },

...

]

type是一个对象,通过一些属性来描述一个token

{

type: {

label: 'name',

keyword: undefined,

beforeExpr: false,

startsExpr: true,

rightAssociative: false,

isLoop: false,

isAssign: false,

prefix: false,

postfix: false,

binop: null,

updateContext: null

},

...

}

语法分析

这一阶段会根据上一阶段生成的tokens构造出AST的表述结构。

相关联的tokens会被组合成语句,形成子树,即子树的根节点是描述表达式的节点,子节点是token产生的节点或者嵌套描述其它表达式的节点。

ASTNode并不是直接复用上述Token的数据结构,而是一个新的数据结构。

这里用AST explorer进行举例。

语句:n*n;

生成的AST如下:

{

"type": "Program",

"start": 0,

"end": 6,

"body": [

{

"type": "ExpressionStatement",

"start": 0,

"end": 6,

"expression": {

"type": "BinaryExpression",

"start": 0,

"end": 5,

"left": {

"type": "Identifier",

"start": 0,

"end": 1,

"name": "n"

},

"operator": "*",

"right": {

"type": "Identifier",

"start": 4,

"end": 5,

"name": "n"

}

}

}

],

"sourceType": "module"

}

语法分析完成之后就得到了抽象语法树。

转换 transform

转换操作通过遍历抽象语法树,对节点进行新增、更新、删除等操作。这是Babel工作流程中最复杂的部分,也是babel插件介入工作的主要部分。

Visitor

Babel使用深度优先遍历 AST,这个过程中使用了访问者模式。即构建一个visitor对象,遍历过程中针对节点的类型,执行不同的方法,而方法又细分为enterexit两个与时间相关的hook

visitor示例:

const MyVisitor = {

Identifier: {

enter() {

...

},

exit() {

...

}

},

CallExpression: {

enter(){

...

},

exit(){

...

}

},

...

};

NodePath

在遍历 AST 的时候,babel还会生成NodePath对象,这个对象包含了节点本身以及与节点相关的上下文信息,比如父节点、兄弟节点和作用域信息等。NodePath对象的数据结构大致如下(不止这些属性):(摘自官方handbook

{

// 当前节点的父节点,表示这是一个函数声明(FunctionDeclaration)

"parent": {

"type": "FunctionDeclaration",

"id": {...}, // 函数声明的标识符节点(具体内容省略)

....

},

// 当前路径对应的 AST 节点,这是一个标识符(Identifier),其名称是 "square"

"node": {

"type": "Identifier",

"name": "square"

},

// 包含处理工具的对象,通常包括 `file` 属性,用于访问文件信息和其他上下文信息

"hub": {...},

// 保存路径上下文的栈,用于处理嵌套的路径操作

"contexts": [],

// 存储与路径相关的自定义数据

"data": {},

// 标记是否应跳过当前路径的遍历

"shouldSkip": false,

// 标记是否应停止整个遍历过程

"shouldStop": false,

// 标记当前节点是否已被删除

"removed": false,

// 在遍历过程中存储插件的状态信息

"state": null,

// 当前路径的选项对象,通常用于配置遍历选项

"opts": null,

// 指示是否应跳过某些子节点

"skipKeys": null,

// 当前节点父节点的 `NodePath` 对象

"parentPath": null,

// 当前路径的上下文信息

"context": null,

// 当前节点所在的容器(可能是父节点的属性或数组)

"container": null,

// 如果当前节点在父节点中是一个列表的一部分,则为列表的键

"listKey": null,

// 布尔值,指示当前节点是否在其父节点的列表中

"inList": false,

// 当前节点在其父节点中的键

"parentKey": null,

// 当前节点在父节点中的位置

"key": null,

// 当前路径的作用域(scope)对象

"scope": null,

// 当前路径节点的类型(在某些上下文中使用)

"type": null,

// 当前路径节点的类型注解(TypeScript 或 Flow)

"typeAnnotation": null,

......

}

自定义插件简单示例:

my-plugin.js

module.exports = function (babel) {

const { types: t } = babel;

return {

visitor: {

Identifier(path) {

// `path` 是一个 `NodePath` 对象,代表当前的标识符节点

if (path.node.name === 'oldName') {

// 修改当前标识符节点的名称

path.replaceWith(t.identifier('newName'));

}

},

},

};

};

  • Identifier如果配置为一个函数,则默认是enter执行;
  • 传入的参数path就是NodePath对象,包含当前节点、上下文、作用域等信息,以及一些更新节点的方法;
  • 相关的方法见官方文档:babel-handbook/translations/en/plugin-handbook.md at master · jamiebuilds/babel-handbook (github.com)
    • 新增兄弟节点:path.insertBeforepath.insertAfter
    • 删除节点:path.remove
    • 更新节点:path.replaceWith

babel配置文件中(例如.babelrc):使用文件路径配置目标插件

{

"presets": ["@babel/preset-env"],

"plugins": ["./my-plugin"]

}

生成 generate

babel通过babel-generator模块深度优先遍历 AST ,根据节点类型生成相应的代码片段,并根据配置选项生成不同格式的代码和源码映射。

这个阶段的产物是:编译后的代码 + source-map

结语

babel的优点在于为JS提供了许多可能性。

对于web前端开发人员来说,他们不再需要过度纠结语法的兼容性,babel会完成代码降级兼容旧版本;

对于babel插件开发人员来说,babel是一个便捷地操作抽象语法树的工具,我们不再需要手写编译器,起码不需要实现parsegenerate,只需要将注意力集中在最核心的visitor,关注如何对 AST 进行 transform

参考资料

[1] babel-handbook/translations/zh-Hans/plugin-handbook.md at master · jamiebuilds/babel-handbook (github.com)

[2] EmberConf 2016: How to Build a Compiler by James Kyle - YouTube

[3] jamiebuilds/the-super-tiny-compiler: ⛄ Possibly the smallest compiler ever (github.com)

[4] https://www.babeljs.cn/docs/

[5] AST explorer

视频 [2] 是 项目[3] 的作者在一个会议上的演讲,视频的内容大部分都是在讲这个项目。


上一篇: 前端进阶:Vue.js

下一篇: html学习

本文标签

随笔    JS    前端知识库    javascript   


声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。