Babel - 相關(guān)基礎(chǔ)介紹

簡介


Babel是通用的多用途的javascript編譯器。不僅如此,它還是一組用于多種不同形態(tài)的靜態(tài)分析模塊。

靜態(tài)分析 是指在不執(zhí)行代碼的前提下分析代碼。(與之相對應(yīng)的就是動態(tài)分析,代碼執(zhí)行過程中分析代碼)。靜態(tài)分析的目的是多樣性的,比如可以用于代碼校驗/編譯/高亮/代碼轉(zhuǎn)換/優(yōu)化/壓縮等等。

讀者可以使用Babel開發(fā)多種不同的工具以幫助提升效率和編寫更好的程序。

基礎(chǔ)


Babel是一種javascript編譯器,確切地說是轉(zhuǎn)換器,即源碼到源碼的編譯器。通俗地說,Babel接收一段javascript代碼并修改它,然后輸出新的符合開發(fā)者預(yù)期的代碼。

ASTs (抽象語法樹)

下列的步驟都會涉及到創(chuàng)建&操作AST

Babel基于EStree操作AST,核心規(guī)范地址位于此處

function square(n) {
  return n * n;
}

點擊查看上述代碼對應(yīng)的AST,有助于理解AST節(jié)點

代碼轉(zhuǎn)成的AST信息如下:

- FunctionDeclaration:
  - id:
    - Identifier:
      - name: square
  - params [1]
    - Identifier
      - name: n
  - body:
    - BlockStatement
      - body [1]
        - ReturnStatement
          - argument
            - BinaryExpression
              - operator: *
              - left
                - Identifier
                  - name: n
              - right
                - Identifier
                  - name: n

相應(yīng)的json格式為:

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

細心的讀者應(yīng)該已經(jīng)發(fā)現(xiàn),AST中不同類型的節(jié)點,結(jié)構(gòu)是相似的

  • 函數(shù)定義
{
  type: "FunctionDeclaration",
  id: {...},
  params: [...],
  body: {...}
}
  • 變量
{
   type: "Identifier",
   name: ...
}
  • 表達式
{
  type: "BinaryExpression",
  operator: ...,
  left: {...},
  right: {...}
}

上述的結(jié)構(gòu)并不是對應(yīng)節(jié)點的全部屬性,主要作為說明,選取部分屬性

AST由一個單獨的Node,或者成百上千個Node構(gòu)成。這些節(jié)點表達了程序的語法信息,基于此可以做程序的靜態(tài)分析。

每一個Node都從基礎(chǔ)的接口派生而出

interface Node {
  type: string;
}

字段type表示節(jié)點的類型(如 FunctionDeclarationIdentifier 等)。不同類型的節(jié)點會定義額外的字段來詳細描述自身。

Babel也會在節(jié)點上添加一些節(jié)點在源碼中的位置信息的屬性,比如下面這種:

{
  type: ...,
  start: 0,
  end: 38,
  loc: {
    start: {
      line: 1,
      column: 0
    },
    end: {
      line: 3,
      column: 1
    }
  },
  ...
}

startend,loc存在于所有的節(jié)點中。

Babel處理的階段

Babel有3個重要的階段parse,transform,generate。

Parse

這個階段將輸入的源碼輸出為AST。包含詞法分析語法分析。

詞法分析

詞法分析的過程是將代碼字符串轉(zhuǎn)換為一串 tokens
以下面的代碼語句為例,tokens可以理解為語法片段的平鋪。

n * n

對應(yīng)為

[
  { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
  ...
]

每一個type包含一系列的屬性,如

{
  type: {
    label: 'name',
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}

token中也包含startendloc信息,這一點跟AST的節(jié)點一樣。

語法分析

語法分析的過程就是把上述的tokens轉(zhuǎn)換成AST?;?code>tokens中的信息,對tokens進行樹形構(gòu)建,使其對使用者更為友好,構(gòu)建產(chǎn)物的結(jié)構(gòu)是樹形結(jié)構(gòu),代表語法片段間的關(guān)系,也就是我們通常說的AST。

Transform

transform階段是對給定的AST進行遍歷,基于特定的目的對AST中的節(jié)點進行添加/修改/刪除。這個部分是Babel或其他編譯器最為復(fù)雜的。這個階段也是Babel插件介入的階段,插件的部分在相關(guān)章節(jié)會詳細闡述,這里就不再贅述。

Generate

代碼生成階段基于最后生成的AST輸出代碼字符串和source maps。生成代碼相對簡單:深度優(yōu)先遍歷AST,構(gòu)建代表相應(yīng)語義的代碼字符串。

Traversal


如果要對AST進行transform,那么就必須遞歸遍歷AST的節(jié)點。
以前文的函數(shù)定義節(jié)點FunctionDeclaration為例,它包含一些屬性:id,paramsbody。而這些屬性也都包含嵌套的節(jié)點。

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

因此,遍歷上述AST的順序如下

  • FunctionDeclaration節(jié)點開始,遍歷其屬性列表
  • 訪問id屬性,類型為Identifier,它沒有子節(jié)點,繼續(xù)訪問
  • 訪問params屬性,類型為節(jié)點數(shù)組,因此遍歷該數(shù)組,每個節(jié)點都是Identifier,繼續(xù)訪問
  • 訪問body屬性,類型為BlockStatement,這個類型有body屬性,為一組節(jié)點,遍歷該節(jié)點數(shù)組
  • BlockStatement只有1個ReturnStatement的節(jié)點,該節(jié)點有argument屬性
  • 訪問argument,其類型為BinaryExpression,包含 operator,leftright屬性。
  • operator不是節(jié)點,繼續(xù)訪問
  • 訪問leftright對應(yīng)的節(jié)點

節(jié)點訪問貫穿于整個transform階段。

Visitors

訪問節(jié)點的概念源于visitor設(shè)計模式。
Visitors是一種跨語言遍歷AST的模式。簡單地說,Visitors是一個對象,其中定義了用于接受AST中特定節(jié)點類型的方法。這么說顯得比較抽象,可以看下面的示意代碼。

// 方法直接定義在對象上
const MyVisitor = {
  Identifier() {
    console.log("Called!");
  }
};

// 先創(chuàng)建對象,再附加屬性
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}

注意: Identifier() { ... } 是對 Identifier: { enter() { ... } }的簡寫。

MyVisitor定義了一個在遍歷(traversal)過程中的基礎(chǔ)訪問器,訪問的每個類型為Identifier的節(jié)點時都會調(diào)用該訪問器。對于下面的示例代碼

function square(n) {
  return n * n;
}

因為包含4個標識符節(jié)點(squaren,n,n),因此Identifier訪問器方法會被調(diào)用4次。

path.traverse(MyVisitor);
// Called!
// Called!
// Called!
// Called!

這些方法調(diào)用都發(fā)生在enter節(jié)點時,也可以在exit節(jié)點時調(diào)用。
假設(shè)有如下的語法樹結(jié)構(gòu)

- FunctionDeclaration
  - Identifier (id)
  - Identifier (params[0])
  - BlockStatement (body)
    - ReturnStatement (body)
      - BinaryExpression (argument)
        - Identifier (left)
        - Identifier (right)

整個的遍歷順序(包含enterexit)的示意如下

  • Enter FunctionDeclaration
    • Enter Identifier (id)
      • Hit dead end
    • Exit Identifier (id)
    • Enter Identifier (params[0])
      • Hit dead end
    • Exit Identifier (params[0])
    • Enter BlockStatement (body)
      • Enter ReturnStatement (body)
        • Enter BinaryExpression (argument)
          • Enter Identifier (left)
            • Hit dead end
          • Exit Identifier (left)
          • Enter Identifier (right)
            • Hit dead end
          • Exit Identifier (right)
        • Exit BinaryExpression (argument)
      • Exit ReturnStatement (body)
    • Exit BlockStatement (body)
  • Exit FunctionDeclaration

因此,訪問器有2次機會訪問1個節(jié)點,即在enterexit的時候。

const MyVisitor = {
  Identifier: {
    enter() {
      console.log("Entered!");
    },
    exit() {
      console.log("Exited!");
    }
  }
};

如果想讓多個訪問器使用同一個方法訪問節(jié)點,可以使用|將不同的訪問器隔開,形如Identifier|MemberExpression。

flow-comments中的使用示例如下:

"ExportNamedDeclaration|Flow"(path) {
    let { node, parent } = path;
    if (t.isExportNamedDeclaration(node) && !t.isFlow(node.declaration)) {
      return;
    }
    wrapInFlowComment(path, parent);
},

訪問器也可以使用在babel-types中定義的節(jié)點別名。

例如:
FunctionFunctionDeclaration,FunctionExpression,ArrowFunctionExpressionObjectMethodClassMethod的別名。

const MyVisitor = {
  Function(path) {}
  //等價于
//'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ObjectMethod|ClassMethod'(path) {}
};
Paths

一個 AST通常有很多節(jié)點,但是節(jié)點之間是如何相互關(guān)聯(lián)的呢?通過一個完全可控的巨大可變對象,對該對象進行操作,同時使用 Paths 來簡化對象的操作。
一個Path代表兩個節(jié)點的關(guān)聯(lián)信息。以下面的結(jié)構(gòu)為例:

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  ...
}

Identifier作為path所包含的信息如下:

{
  // 記錄父節(jié)點的信息,可以通過 parent 快速訪問到 FunctionDeclaration
  "parent": {
    "type": "FunctionDeclaration",
    "id": {...},
    ....
  },
  "node": {
    "type": "Identifier",
    "name": "square"
  }
}

上面只是一個簡單的示意,完整的path還包含其他信息

{
  "parent": {...},
  "node": {...},
  "hub": {...},
  "contexts": [],
  "data": {},
  "shouldSkip": false,
  "shouldStop": false,
  "removed": false,
  "state": null,
  "opts": null,
  "skipKeys": null,
  "parentPath": null,
  "context": null,
  "container": null,
  "listKey": null,
  "inList": false,
  "parentKey": null,
  "key": null,
  "scope": null,
  "type": null,
  "typeAnnotation": null
}

還有其他涉及到節(jié)點添加/更新/移動/刪除的各種方法,這里就不贅述,在后續(xù)相關(guān)章節(jié)討論。
某種意義上講,paths是包含節(jié)點在AST上的位置以及節(jié)點各種信息的響應(yīng)式對象。更新AST的同時,這些相關(guān)的信息也會同步更新。Babel將這些同步更新對開發(fā)者透明,使得操作節(jié)點盡量簡單。

Paths in Visitors

在訪問器的方法中,實際訪問的是path,而非node本身。比如:

  • 定義訪問器
const MyVisitor = {
  Identifier(path) {
    console.log("Visiting: " + path.node.name);
  }
};
  • 待遍歷的源碼
a + b + c
  • 遍歷
path.traverse(MyVisitor);
// Visiting: a
// Visiting: b
// Visiting: c

在對AST進行transform時,要特別關(guān)注State,如果處理不當,會導(dǎo)致實際處理的結(jié)果跟預(yù)期不一致。
以下面的代碼為例

function square(n) {
  return n * n;
}

現(xiàn)在有個轉(zhuǎn)換需求,將變量n變成x,使用前述的訪問器來處理。

let paramName;

const MyVisitor = {
  FunctionDeclaration(path) {
    const param = path.node.params[0];
    paramName = param.name;
    param.name = "x";
  },

  Identifier(path) {
    if (path.node.name === paramName) {
      path.node.name = "x";
    }
  }
};

使用函數(shù)定義訪問器和變量訪問器,修改函數(shù)的參數(shù)名和函數(shù)體中的變量名。
但是如果代碼變成下面這樣呢?

function square(n) {
  return n * n;
}
const n = 3;

如果使用上面的訪問器,就會出問題。因為在square下面有另外一個變量名為n的變量。但我們的需求是修改函數(shù)中的變量n,這種情況下比較合理的方案是在訪問器中嵌套訪問器。

const updateParamNameVisitor = {
  Identifier(path) {
    if (path.node.name === this.paramName) {
      path.node.name = "x";
    }
  }
};

const MyVisitor = {
  FunctionDeclaration(path) {
    const param = path.node.params[0];
    const paramName = param.name;
    param.name = "x";
    // 在訪問器中,向下遞歸訪問子節(jié)點
    path.traverse(updateParamNameVisitor, { paramName });
  }
};

path.traverse(MyVisitor);

上面的示例演示了全局狀態(tài)對于訪問器的影響,同時也給出了相應(yīng)的消除外部狀態(tài)影響的方案。

Scopes

javascript有詞法作用域。詞法作用域是一個由不同的塊創(chuàng)建而成的樹形結(jié)構(gòu)。

// global scope

function scopeOne() {
  // scope 1

  function scopeTwo() {
    // scope 2
  }
}

在javascript中創(chuàng)建的引用(可以是 變量/函數(shù)//變量 等)屬于當前作用域。

var global = "I am in the global scope";

function scopeOne() {
  var one = "I am in the scope created by `scopeOne()`";

  function scopeTwo() {
    var two = "I am in the scope created by `scopeTwo()`";
  }
}

代碼中使用當前作用域外層的變量引用

function scopeOne() {
  // scopeOne下的變量
  var one = "I am in the scope created by `scopeOne()`";

  function scopeTwo() {
    // 引用外部的變量也是可以的,會修改外部引用的值
    one = "I am updating the reference in `scopeOne` inside `scopeTwo`";
  }
}

代碼中定義與外層變量同名的變量

function scopeOne() {
  var one = "I am in the scope created by `scopeOne()`";

  function scopeTwo() {
    // 定義同名的變量,這里的修改不會修改到外部的同名引用
    var one = "I am creating a new `one` but leaving reference in `scopeOne()` alone.";
  }
}

transform時要警惕變量的作用域,避免修改代碼時導(dǎo)致破壞其他部分的代碼。換言之,對變量的處理需要在它所屬的作用域內(nèi)。

作用的結(jié)構(gòu)如下所示

{
  path: path,
  block: path.node,
  parentBlock: path.parent,
  parent: parentScope,
  bindings: [...]
}

創(chuàng)建一個新的作用域時,會賦予其對應(yīng)的path和關(guān)聯(lián)父作用域。遍歷時收集其作用域內(nèi)所有的引用(bindings)。

Bindings

引用都是在特定的作用域內(nèi)的,引用跟作用域之間的關(guān)系叫做binding,簡單理解就是把引用綁定到作用域上。

function scopeOnce() {
  var ref = "This is a binding";

  ref; // This is a reference to a binding

  function scopeTwo() {
    ref; // This is a reference to a binding from a lower scope
  }
}

binding的結(jié)構(gòu)如下

{
  identifier: node,
  scope: scope,
  path: path,
  kind: 'var',

  referenced: true,
  references: 3,
  referencePaths: [path, path, path],

  constant: false,
  constantViolations: [path]
}

通過上面的binding信息,可以獲取很多信息

  • binding的類型(參數(shù)/變量等)
  • 所有對該binding的引用
  • 所屬的作用域
  • 判斷變量是否為常量(引用是否在某個地方被修改
  • 修改變量的path列表(哪些地方修改了引用值
  • ...

判斷binding是否constant在很多場景下非常有用,比如在代碼壓縮的應(yīng)用中影響巨大(修改引用的名稱時,需要將引用到它的地方變量名一起修改)。

function scopeOne() {
  // 創(chuàng)建binding,無代碼對其引用修改,固constant為true
  var ref1 = "This is a constant binding";

  becauseNothingEverChangesTheValueOf(ref1);

  function scopeTwo() {
    // 創(chuàng)建binding
    var ref2 = "This is *not* a constant binding";
    // 修改引用ref2,導(dǎo)致binding的constant為false
    ref2 = "Because this changes the value";
  }
}

至此,初步翻譯Babel文檔中相關(guān)的基本概念和示例代碼理解,如果錯誤歡迎交流指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容