Babel 內部機制研究

Babel

Babel 是一個 JavaScript 編譯器。node

// Babel 輸入: ES2015 箭頭函數
[1, 2, 3].map((n) => n + 1);

// Babel 輸出: ES5 語法實現的同等功能
[1, 2, 3].map(function(n) {
  return n + 1;
});
複製代碼

Babel經過轉換,讓咱們寫新版本的語法,轉換到低版本,這樣就能夠在只支持低版本語法的瀏覽器裏運行了。git

Babel真厲害,它竟然‘認識’代碼、更改代碼。那Babel就是操做代碼的代碼,酷。github

學習Babel對咱們能力的提高有很大幫助,咱們平時都是用代碼來操做各類東西,此次咱們操做的對象,變成了代碼自己。算法

這個認識和操做代碼的過程,學名叫作代碼的靜態分析。express

代碼的靜態分析

先來看一個問題,編輯器裏代碼的高亮,以下:npm

image

編輯器能夠把代碼不一樣的成員,標記爲不一樣的顏色。顯然編輯器要‘認識’代碼,對代碼進行分析和處理,才能達到這種效果。這就叫代碼的靜態分析。瀏覽器

靜態分析 VS 動態分析

靜態分析是在不須要執行代碼的前提下對代碼進行分析的處理過程。bash

動態分析是在代碼的運行過程當中對代碼進行分析和處理。上面的代碼高亮屬於靜態分析,在代碼沒有運行的狀況下,進行分析和處理的。babel

靜態分析的用處

靜態分析不光能高亮代碼,還有就是代碼轉換,還能夠對咱們的源代碼進行優化、壓縮等操做。數據結構

AST (抽象語法樹)

在對代碼靜態分析的過程當中,要將源碼轉換成AST (抽象語法樹)。

爲何會有AST

源代碼對於Babel來講,就是一個字符串。Babel要對這個字符串進行分析。咱們平時對字符串的操做,就是使用字符串方法或是正則,但相對字符串(源碼)進行復雜的操做,遠遠不夠。

須要將字符串(源碼)轉換成樹的數據結構,纔好操做。這個樹結構,就叫AST(抽象語法樹)。

這裏我想到 程序 = 數據結構 + 算法。咱們平時寫的業務需求,對數據結構要求不高,簡單的對象和列表就能夠搞定,但要某些特定的複雜問題,好比如今研究的操做代碼,就需先思考:我應該把操做的事物放到什麼樣的數據結構上,才更容易我寫算法/邏輯。

把源碼解析成AST

對於源碼,此時咱們就把它看出一個字符串,對其分析的第一步,確定是先把源碼轉換成AST,纔好後續操做。

有一個在線AST轉換器,咱們在這上面能夠作實驗,寫出的代碼,它就幫咱們翻譯成AST:

我什麼都不寫,AST就有一個根結點了:

// AST
{
  "type": "Program",
  "start": 0,
  "end": 0,
  "body": [],
  "sourceType": "module"
} // 能夠當作是一個對象,有一些字段,這代碼樹的根結點。
複製代碼

而後我寫一句代碼:

// 源碼
const text = 'Hello World';

// AST
{
  "type": "Program",
  "start": 0,
  "end": 27,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 27,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 26,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 10,
            "name": "text"
          },
          "init": {
            "type": "Literal",
            "start": 13,
            "end": 26,
            "value": "Hello World",
            "raw": "'Hello World'"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}
複製代碼

懵逼,一句const text = 'Hello World'; 就生成這麼多東西。看懂它、理解AST是學習Babel的第一個門檻。

理解AST

看下圖,這就是AST Explorer的界面,左邊寫代碼,右邊就幫助咱們翻譯成AST,AST有兩種表達方式,Tree和JSON,上面都是用JSON形式表示AST,後來我發現仍是用Tree的形式看更容易些,由於Tree的形式更突出節點的類型:

image

我將AST表達的樹畫出來,以下:

image

總結AST樹的特色:

  1. 節點是有類型的。咱們學習樹這種數據結構時,節點都是最簡單的,這裏複雜了,有類型。
  2. 節點與子節點的關係,是經過節點的屬性連接的。咱們學習的樹結構,都是left、right左孩子右孩子的。可是AST樹,不一樣類型的節點,屬性不一樣,Program類型節點的子節點是它的body屬性,VariableDeclaration類型的子節點,是它的declarations、kind屬性。也就是節點的屬性看做是節點的子節點,而且子節點也可能有類型,近而造成一個樹。
  3. 父節點是全部子節點的組合,咱們能夠看到VariableDeclaration表明的const text = 'Hello World'被拆分紅了下面兩個子節點,子節點又繼續拆分。

但願能從上面的分析中,讓你們對AST有一個最直觀的認識,就是節點有類型的樹。

那麼節點的類型系統就很必要了解了,這裏是Babel的AST類型系統說明。你們能夠看看,能夠說類型系統是抽象了代碼的各類成員,標識符、字面量、聲明、表達式。因此擁有這些類型的節點的樹結構,能夠用來表達咱們的代碼。

參照類型系統,多實驗,咱們就會對AST的結構大致掌握和理解了。

額外:V8引擎也用到AST

額外提一下,V8中也用到了AST。V8引擎有四個主要模塊:

  1. 轉換器Paser:將源代碼轉換成AST。
  2. 解釋器:將AST轉換爲Bytecode。
  3. 編譯器:將Bytecode轉換爲彙編代碼。
  4. 垃圾回收模塊:負責管理內存空間回收。

能夠看到AST也是V8執行的關鍵一環。下面下來看看Babel對於AST的利用,及運行步驟。

Babel 的處理步驟

回看Babel的處理過程

  1. 解析(parse)。將源代碼變成AST。
  2. 轉換(transform)。操做AST,這也是咱們能夠操做的部分,去改變代碼。
  3. 生成(generate)。將更改後的AST,再變回代碼。

解析器 babylon

第一步:解析,Babel中的解析器是babylon。咱們來體驗一下:

// 安裝
npm install --save babylon
複製代碼
// 實驗代碼
import * as babylon from "babylon";

const code = `const text = 'Hello World';`;

const ast = babylon.parse(code);

console.log('ast', ast);

複製代碼

code變量是咱們的源代碼,ast變量是AST,咱們看一下打印結果:

image

和咱們預期的同樣,獲得AST了。這裏我注意到還有start、end、loc這樣位置信息的字段,應該能夠對生成Source Map有用的字段。

轉換器 babel-traverse

第二步:轉換。獲得ast了,該操做它了,Babel中的babel-traverse用來幹這個事。

// 安裝
npm install --save babel-traverse
複製代碼
// 實驗代碼
import * as babylon from "babylon";
import traverse from "babel-traverse";

const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    console.log('path', path);
  }
})

console.log('ast', ast);
複製代碼

babel-traverse庫暴露了traverse方法,第一個參數是ast,第二個參數是一個對象,咱們寫了一個enter方法,方法的參數是個path,咋不是個node呢?咱們看一下輸出:

image

path被打印了5次,ast上確實也是有5個節點,是對應的。traverse方法是一個遍歷方法,path封裝了每個節點,而且還提供容器container,做用域scope這樣的字段。提供個更多關於節點的相關的信息,讓咱們更好的操做節點。

咱們來作一個變量重命名操做:

import * as babylon from "babylon";
import traverse from "babel-traverse";

const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    if (path.node.type === "Identifier"
      && path.node.name === 'text') {
      path.node.name = 'alteredText';
    }
  }
})

console.log('ast', ast);
複製代碼

看結果:

image

確實咱們的ast被更改了,用這個ast生成的code就會是const alteredText = 'Hello World';

babel-traverse的Lodash : babel-types

在利用babel-traverse操做AST時,也能夠利用工具庫幫助咱們寫出更加簡潔有效的代碼,就可使用babel-types。

npm install --save babel-types
複製代碼
import * as babylon from "babylon";
import traverse from "babel-traverse";
import * as t from "babel-types";

const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    // 使用babel-types
    if (t.isIdentifier(path.node, { name: "text" })) {
      path.node.name = 'alteredText';
    }
  }
})

console.log('ast', ast);
複製代碼

使用babel-types實現和上例中同樣的功能,代碼量更少了。

生成器 babel-generator

第三步:生成。獲得操做後的ast,該生成新代碼了。Babel中的babel-generator用來幹這個事。

npm install --save babel-generator
複製代碼
// 加入babel-generator
import * as babylon from "babylon";
import traverse from "babel-traverse";
import * as t from "babel-types";
import generate from "babel-generator";

const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    if (t.isIdentifier(path.node, { name: "text" })) {
      path.node.name = 'alteredText';
    }
  }
})

const genCode = generate(ast, {}, code);

console.log('genCode', genCode);
複製代碼

來看打印結果:

image

nice! 在code字段裏,咱們看到裏新生成的代碼。

固然,上面提到的這四個庫,還有更多細節,有興趣的能夠再研究研究。

這些庫的集合,就是咱們的Babel。

插件

咱們研究過了Babel的內部,如今跳出來,咱們須要在外部操做AST,而不是進Babel內部去改traverse。

從外部操做AST,就須要插件了。咱們來研究一個插件babel-plugin-transform-member-expression-literals

使用次插件後:

// 轉換前
obj.const = "isKeyword";

// 轉換後
obj["const"] = "isKeyword";
複製代碼

咱們再來看看這個插件的源碼

{
    name: "transform-member-expression-literals",
    visitor: {
      MemberExpression: {
        exit({ node }) {
          const prop = node.property;
          if (
            !node.computed &&
            t.isIdentifier(prop) &&
            !t.isValidES3Identifier(prop.name)
          ) {
            // foo.default -> foo["default"]
            node.property = t.stringLiteral(prop.name);
            node.computed = true;
          }
        },
      },
    },
  };
}
複製代碼

這裏我嘗試將它放到咱們的實驗代碼裏,以下:

import * as babylon from "babylon";
import traverse from "babel-traverse";
import * as t from "babel-types";
import generate from "babel-generator";

const code = `const obj = {};obj.const = "isKeyword";`;
const ast = babylon.parse(code);
const plugin = {
  MemberExpression: {
    exit({ node }) {
      const prop = node.property;
      console.log('node', node);
      if (
        !node.computed &&
        t.isIdentifier(prop)
        // !t.isValidES3Identifier(prop.name) 這裏註釋掉,咱們的t裏沒這個方法
      ) {
        // foo.default -> foo["default"]
        node.property = t.stringLiteral(prop.name);
        node.computed = true;
      }
    },
  },
};

traverse(ast, plugin)

const genCode = generate(ast, {}, code);

console.log('genCode', genCode); 
複製代碼

輸出的代碼是"const obj = {};obj["const"] = "isKeyword";"。符合預期,也就是說,Babel的插件,會傳給內部的traverse方法。而且是一種符合訪問者模式的,讓咱們能夠針對節點類型(如這裏的visitor.MemberExpression)的操做。這裏用的是exit,而不是enter了,解釋一下,traverse是對樹的深度遍歷,向下遍歷這棵樹咱們進入(entry)每一個節點,向上遍歷回去時咱們退出(exit)每一個節點。就是對於AST,traverse遍歷了兩遍,咱們能夠選擇在進入仍是退出的時候,操縱節點。

結束語

個人參考:

  1. Babel用戶手冊
  2. Babel中文文檔

今天的研究就到這裏,理解Babel內部機制和基本的插件工做方式。

相關文章
相關標籤/搜索