翼度科技»论坛 编程开发 JavaScript 查看内容

[babel] babel的工作原理

4

主题

4

帖子

12

积分

新手上路

Rank: 1

积分
12
Babel是什么

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

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的功能(通过引入第三方 polyfill 模块,例如 core-js
  • 源码转换(codemods)
例如我们在React经常使用 JSX 语法,由于这不是JS原生语法,所以不能直接被 JS 引擎编译执行。在代码被执行之前,需要使用编译器进行转译,转换成 JS 代码。
Babel的工作原理

从代码到代码的过程,也是从字符串到字符串的过程。当然不可能直接在字符串上进行操作。中间过程生成了抽象语法树(Abstract Syntax Tree,简称AST),用树来表示代码的结构和语义。
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除了记录类型和值,还记录了这个词在源代码中的位置,即start、end、loc等属性。
token是指代码中独立的最小单元,它可以是数字字面量(NumberLiteral)、字符串字面量(StringLiteral)、操作符(operator)等等。
  1. export class Token {
  2.   constructor(state: State) {
  3.     this.type = state.type;
  4.     this.value = state.value;
  5.     this.start = state.start;
  6.     this.end = state.end;
  7.     this.loc = new SourceLocation(state.startLoc, state.endLoc);
  8.   }
  9.   declare type: TokenType;
  10.   declare value: any;
  11.   declare start: number;
  12.   declare end: number;
  13.   declare loc: SourceLocation;
  14. }
复制代码
在词法分析完成之后,会得到一个tokens数组,记录了代码中每个词的记录。
比如下面这个简单的语句:
  1. n * n;
复制代码
词法分析之后将得到如下的数组:
  1. [
  2.   { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
  3.   { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  4.   { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
  5.   ...
  6. ]
复制代码
type是一个对象,通过一些属性来描述一个token:
  1. {
  2.   type: {
  3.     label: 'name',
  4.     keyword: undefined,
  5.     beforeExpr: false,
  6.     startsExpr: true,
  7.     rightAssociative: false,
  8.     isLoop: false,
  9.     isAssign: false,
  10.     prefix: false,
  11.     postfix: false,
  12.     binop: null,
  13.     updateContext: null
  14.   },
  15.   ...
  16. }
复制代码
语法分析

这一阶段会根据上一阶段生成的tokens构造出AST的表述结构。
相关联的tokens会被组合成语句,形成子树,即子树的根节点是描述表达式的节点,子节点是token产生的节点或者嵌套描述其它表达式的节点。
AST的Node并不是直接复用上述Token的数据结构,而是一个新的数据结构。
这里用AST explorer进行举例。
语句:n*n;
生成的AST如下:
  1. {
  2.   "type": "Program",
  3.   "start": 0,
  4.   "end": 6,
  5.   "body": [
  6.     {
  7.       "type": "ExpressionStatement",
  8.       "start": 0,
  9.       "end": 6,
  10.       "expression": {
  11.         "type": "BinaryExpression",
  12.         "start": 0,
  13.         "end": 5,
  14.         "left": {
  15.           "type": "Identifier",
  16.           "start": 0,
  17.           "end": 1,
  18.           "name": "n"
  19.         },
  20.         "operator": "*",
  21.         "right": {
  22.           "type": "Identifier",
  23.           "start": 4,
  24.           "end": 5,
  25.           "name": "n"
  26.         }
  27.       }
  28.     }
  29.   ],
  30.   "sourceType": "module"
  31. }
复制代码
语法分析完成之后就得到了抽象语法树。
转换 transform

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

Babel使用深度优先遍历 AST,这个过程中使用了访问者模式。即构建一个visitor对象,遍历过程中针对节点的类型,执行不同的方法,而方法又细分为enter和exit两个与时间相关的hook。
visitor示例:
  1. const MyVisitor = {
  2.   Identifier: {
  3.     enter() {
  4.       ...
  5.     },
  6.     exit() {
  7.       ...
  8.     }
  9.   },
  10.   CallExpression: {
  11.     enter(){
  12.       ...
  13.     },
  14.     exit(){
  15.       ...
  16.     }
  17.   },
  18.   ...
  19. };
复制代码
NodePath

在遍历 AST 的时候,babel还会生成NodePath对象,这个对象包含了节点本身以及与节点相关的上下文信息,比如父节点、兄弟节点和作用域信息等。NodePath对象的数据结构大致如下(不止这些属性):(摘自官方handbook)
  1. {
  2.   // 当前节点的父节点,表示这是一个函数声明(FunctionDeclaration)
  3.   "parent": {
  4.     "type": "FunctionDeclaration",
  5.     "id": {...},  // 函数声明的标识符节点(具体内容省略)
  6.     ....
  7.   },
  8.   
  9.   // 当前路径对应的 AST 节点,这是一个标识符(Identifier),其名称是 "square"
  10.   "node": {
  11.     "type": "Identifier",
  12.     "name": "square"
  13.   },
  14.   
  15.   // 包含处理工具的对象,通常包括 `file` 属性,用于访问文件信息和其他上下文信息
  16.   "hub": {...},
  17.   
  18.   // 保存路径上下文的栈,用于处理嵌套的路径操作
  19.   "contexts": [],
  20.   
  21.   // 存储与路径相关的自定义数据
  22.   "data": {},
  23.   
  24.   // 标记是否应跳过当前路径的遍历
  25.   "shouldSkip": false,
  26.   
  27.   // 标记是否应停止整个遍历过程
  28.   "shouldStop": false,
  29.   
  30.   // 标记当前节点是否已被删除
  31.   "removed": false,
  32.   
  33.   // 在遍历过程中存储插件的状态信息
  34.   "state": null,
  35.   
  36.   // 当前路径的选项对象,通常用于配置遍历选项
  37.   "opts": null,
  38.   
  39.   // 指示是否应跳过某些子节点
  40.   "skipKeys": null,
  41.   
  42.   // 当前节点父节点的 `NodePath` 对象
  43.   "parentPath": null,
  44.   
  45.   // 当前路径的上下文信息
  46.   "context": null,
  47.   
  48.   // 当前节点所在的容器(可能是父节点的属性或数组)
  49.   "container": null,
  50.   
  51.   // 如果当前节点在父节点中是一个列表的一部分,则为列表的键
  52.   "listKey": null,
  53.   
  54.   // 布尔值,指示当前节点是否在其父节点的列表中
  55.   "inList": false,
  56.   
  57.   // 当前节点在其父节点中的键
  58.   "parentKey": null,
  59.   
  60.   // 当前节点在父节点中的位置
  61.   "key": null,
  62.   
  63.   // 当前路径的作用域(scope)对象
  64.   "scope": null,
  65.   
  66.   // 当前路径节点的类型(在某些上下文中使用)
  67.   "type": null,
  68.   
  69.   // 当前路径节点的类型注解(TypeScript 或 Flow)
  70.   "typeAnnotation": null,
  71.       
  72.   ......
  73. }
复制代码
自定义插件简单示例:
my-plugin.js
  1. module.exports = function (babel) {
  2.   const { types: t } = babel;
  3.   return {
  4.     visitor: {
  5.       Identifier(path) {
  6.         // `path` 是一个 `NodePath` 对象,代表当前的标识符节点
  7.         if (path.node.name === 'oldName') {
  8.           // 修改当前标识符节点的名称
  9.           path.replaceWith(t.identifier('newName'));
  10.         }
  11.       },
  12.     },
  13.   };
  14. };
复制代码
在babel配置文件中(例如.babelrc):使用文件路径配置目标插件
  1. {
  2.   "presets": ["@babel/preset-env"],
  3.   "plugins": ["./my-plugin"]
  4. }
复制代码
生成 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] 的作者在一个会议上的演讲,视频的内容大部分都是在讲这个项目。

来源:https://www.cnblogs.com/feixianxing/p/18335162/babel-compiler-and-plugin
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具