首页 > 其他分享 >[babel] babel的工作原理

[babel] babel的工作原理

时间:2024-07-31 18:06:45浏览次数:13  
标签:... AST babel 工作 原理 null type 节点

Babel是什么

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

常见的用途有:

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

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

image-20240731143915451

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除了记录类型和值,还记录了这个词在源代码中的位置,即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'));
        }
      },
    },
  };
};

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] 的作者在一个会议上的演讲,视频的内容大部分都是在讲这个项目。

标签:...,AST,babel,工作,原理,null,type,节点
From: https://www.cnblogs.com/feixianxing/p/18335162/babel-compiler-and-plugin

相关文章

  • Java并发(十六)一文搞懂Java 线程池原理
    简介什么是线程池线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。为什么要用线程池如果并发请求数量很多,但每个线程执行的时间很短,就会出现频繁的创建和销毁线程。如此一来,会大大降低系统的效率,可能频繁创建和销毁线程的时间......
  • RPC核心原理
    什么是RPCRPC就是远程过程调用RPC的作用屏蔽远程调用和本地调用的区别,让我们感觉就是调用本地项目内的方法。隐藏底层网络通信的复杂性,让我们更专注于业务逻辑。RPC通信流程一个完整的RPC会涉及哪些步骤呢?在调用方程序中,RPC框架根据调用的服务接口提前生成动态代理实现类......
  • 计算机Java项目|基于SpringBoot的科研工作量管理系统
    作者简介:Java领域优质创作者、CSDN博客专家、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、多年校企合作经验,被多个学校常年聘为校外企业导师,指导学生毕业设计并参与学生毕业答辩指导,有较为丰富的相关经验。期待与各位高校教师、企业......
  • 手写MyBatis 重要基本原理框架
    1.手写MyBatis重要基本原理框架@目录1.手写MyBatis重要基本原理框架1.1第一步:IDEA中创建模块1.2第二步:资源工具类,方便获取指向配置文件的输入流1.3第三步:定义SqlSessionFactoryBuilder类1.4第四步:分析SqlSessionFactory类中有哪些属性1.5第五步:定义JDBCTransaction1.6......
  • 5.为工作使用正确的控件
    在本章中,我们将首先考虑WindowsPresentationFoundation(WPF)为我们提供的现有控件,并了解如何使用它们来创建我们需要的布局。我们将研究修改这些控件的多种方法,以避免创建新控件。我们将检查现有控件中内置的各个级别的功能,然后发现如何在需要时最好地声明我们自己的......
  • MQTT原理及案例
    MQTT协议是当今世界上最受欢迎的物联网协议,没有之一。MQTT协议为设备提供了稳定、可靠、简单易用的通信基础,截至目前通过MQTT协议连接的设备已经过亿,广泛应用于IoT、M2M等领域。本篇将从最基础的知识开始,向您讲解MQTT协议的原理与应用。目前MQTT主流版本有MQTT3.1.1......
  • 建筑电气中智能应急照明系统工作原理和应用
    摘要:建筑工程在不断得增多,因此建筑电气需求也在不断增多,在进行建筑电气安装设计过程中,需要对消防应急照明系统的设计进行科学合理的安排,要其具备一定的应急以及消防作用,一旦建筑出现火灾时,或是其他影响照明系统是,建筑电气智能应急照明系统时能够起到准确的疏散作用,延长疏散时......
  • 这本vue3编译原理开源电子书,初中级前端竟然都能看懂
    前言众所周知vue提供了很多黑魔法,比如单文件组件(SFC)、指令、宏函数、cssscoped等。这些都是vue提供的开箱即用的功能,大家平时用这些黑魔法的时候有没有疑惑过一些疑问呢。我们每天写的vue代码一般都是写在*.vue文件中,但是浏览器却只认识html、css、js等文件类型,明显是不认......
  • Java跨平台原理
    Java源代码编译成字节码编译过程:Java源代码(.java文件)首先被Java编译器(javac)编译成一种中间代码,即字节码(.class文件)。这种字节码是一种与具体平台无关的代码,它可以在任何安装了Java虚拟机(JVM)的平台上被解释执行。字节码特性:字节码是Java实现跨平台的关键。它是一种介于源代码和......
  • 手把手教你玩转ESP8266(原理+驱动)
    在嵌入式开发中,无线通信的方式有很多,其中WIFI是绕不开的话题。说到WIFI通信,就不得不提ESP8266了。ESP8266是一款高性能的WIFI串口模块,实现透明传输。只要有一定的串口知识,不需要知道WIFI原理就可以上手,在业内应用广泛。1.源码下载STM32F103C8T6模板工程链接:https......