babel脚本实现原理

为什么要用babel转换代码

我们之前做一些兼容都会都会接触一些 Polyfill 的概念,比如如果某个版本的浏览器不支持 Array.prototype.find 方法,但是我们的代码中有用到 Array 的 find 函数,为了支持这些代码,我们会人为的加一些兼容代码。

if (!Array.prototype.find) {
    Object.defineProperty(Array.prototype, 'find', {
        // todo someting
    })
}

对于这种情况做兼容也很好实现,引入一个 Polyfill 文件就可以了,但是有一些情况我们使用到了一些新语法,或者一些其他写法

// 箭头函数
var a = () => {}
// jsx
var component = () => 

这种情况靠 Polyfill,因为一些浏览器根本就不识别这些代码,这时候就需要把这些代码转换成浏览器识别的代码。 babel就是做这个事情的。

babel做了哪些事情babel做了哪些事情

babel脚本实现原理_第1张图片
image

为了转换我们的代码, babel做了三件事:

  • Parser 解析我们的代码转换为 AST。

  • Transformer 利用我们配置好的 plugins/presets把 Parser生成的 AST转变为新的 AST。

  • Generator 把转换后的 AST生成新的代码

从图上看 Transformer 占了很大一块比重,这个转换过程就是 babel中最复杂的部分,我们平时配置的 plugins/presets就是在这个模块起作用。

从简单的说起

可以看到要想搞懂 babel, 就是去了解上面三个步骤都是在干什么,我们先把比较容易看懂的地方开始了解一下。

Parser 解析

解析步骤接收代码并输出 AST,这其中又包含两个阶段词法分析和语法分析。词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流。语法分析阶段会把一个令牌流转换成 AST 的形式,方便后续操作。

Generator 生成

代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

babel的核心内容

看起来 babel的主要工作都集中在把解析生成的 AST经过 plugins/presets然后去生成 新的AST这上面了。

AST抽象语法树(AbstractSyntaxTree)

我们想象一下要表示上述代码应该是什么样子,首先必须有东西可以表示这些具体的 声明, 变量, 常量的具体信息,比如

var a = 1 + 2

一个声明语句,声明类型是var,左侧是变量,右侧是表达式。有了这些信息我们就可以还原这个程序,这也是把代码解析成 AST时候所做的事情,对应上面我们说的 词法分析语法分析

AST 在线解析

AST Node节点说明

节点

看这个文档时候我们可以看到说明大多是类似这种

interface Node {
  type: string;
  loc: SourceLocation | null;
}

这里提到 interface这个我们在其他语言中是比较常见的,比如 Node规定了 type和 loc属性,如果其他节点继承自 Node,那么它也会实现 type和 loc属性就是说继承自 Node的节点也会有这些属性,基本所有节点都继承自 Node,所以我们基本可以看到 loc这个属性 loc表示个一些位置信息

节点遍历

babel拿到抽象语法树后会使用 babel-traverse进行递归的树状遍历,对于每一个节点都会向下遍历到尽头,然后向上遍历退出分支去寻找下一个分支。这样确保我们能找到任何一个节点,也就是能访问到我们代码的任何一个部分。可是我们要怎么去完成修改操作呢, babel给我们提供了下面这两个概念。

如何写一个plugin

visitor

我们已经知道 babel会遍历节点组成的抽象语法树,每一个节点都会有自己对应的 type,比如变量节点 Identifier等。我们需要给 babel提供一个 visitor对象,在这个对象上面我们以这些节点的 type做为 key,已一个函数作为值,类似如下:

const visitor = {
    Identifier: {
        enter() {
            console.log(
                'traverse enter a Identifier node!'
            )
        },

        exit() {
            console.log(
                'traverse exit a Identifier node!'
            )
        }
    }
}

这样在遍历进入到对应到节点时候, babel就会去执行对应的 enter函数,向上遍历退出对应节点时候, babel就会去执行对应的 exit函数,接着上面的代码我们可以做一个测试

const MyVisitor = {
    visitor
}

const result = babel.transform(code, {
    plugins: [
        MyVisitor
    ]
})

console.log(result.code)

Paths

visitor在遍历到对应节点执行对应函数时候会给我们传入 path参数。它传入的 path参数看起来是这样的:

{
    "parent": {
        "type": "VariableDeclarator",
        "id": {...
        },
    
        ....
    },
    "node": {
        "type": "Identifier",
        "name": "..."
    }
}

从上面我们可以看到 path 表示两个节点之间的连接,通过这个对象我们可以访问到节点、父节点以及进行一系列跟节点操作相关的方法。

babel为了方便我们开发,在每一个环节都有很多人性化的定义也提供了很多实用性的工具,比如之前我们在定义 visitor时候分别定义了 enter, exit函数,可很多时候我们其实只用到了一次在 enter的时候做一些处理就行了。所以我们如果我们直接定义节点的 key为函数,就相当于定义了 enter函数。

const visitor = {
    Identifier: {
        enter(path) {
            // todo
        }
    }
}

// 等同于
const visitor = {
    Identifier() {
        // todo
    }
}

bable-core API参见

bable-types API参见

bable相关核心库与组件

你可能感兴趣的:(babel脚本实现原理)