面試官: 你瞭解過Babel嗎?寫過Babel外掛嗎? 答: 沒有。卒

Axetroy發表於2018-02-25

也就前兩天,面試大廠,其中有那麼一個問題:

  1. 你瞭解過Babel嗎?

瞭解過抽象語法樹,又稱AST,有學習過,也寫過一個基於AST的乞丐版模板引擎,先是詞法解析token,然後生產抽象語法樹,然後更改抽象語法樹,當然這是外掛做的事情,最後根據新的AST生成程式碼。

  1. 寫過Babel外掛嗎

沒有,只是看過相關文件

  1. 如果讓你寫一個外掛,你能寫的出來嗎?

應該可以吧...

遂卒....

開玩笑的,既然提到了,又沒回答上來什麼,哎喲我這暴脾氣,一想到今晚就睡不著,連夜把它擼了。

那麼我們來從零寫個外掛吧。

寫一個預計算簡單表示式的外掛

預覽

Before:

const result = 1 + 2 + 3 + 4 + 5;
複製程式碼

After:

const result = 15;
複製程式碼

以上的例子可能大家不會經常遇到,因為傻x才會這麼寫,但是有可能你會這麼寫

setTimeout(function(){
  // do something
}, 1000 * 2) // 外掛要做的事,就是把 1000 * 2 替換成 2000
複製程式碼

前提條件

開工

再寫程式碼之前,你需要明白Babel它的原理,簡單點說: Babel解析成AST,然後外掛更改AST,最後由Babel輸出程式碼

那麼Babel的外掛模組需要你暴露一個function,function內返回visitor

module.export = function(babel){
  return {
    visitor:{
    }
  }
}
複製程式碼

visitor是對各型別的AST節點做處理的地方,那麼我們怎麼知道Babel生成了的AST有哪些節點呢?

很簡單,你可以把Babel轉換的結果列印出來,或者這裡有傳送門: AST explorer

1

這裡我們看到 const result = 1 + 2中的1 + 1是一個BinaryExpression節點,那麼在visitor中,我們就處理這個節點

var babel = require('babel-core');
var t = require('babel-types');

const visitor = {
  BinaryExpression(path) {
    const node = path.node;
    let result;
    // 判斷表示式兩邊,是否都是數字
    if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {
      // 根據不同的操作符作運算
      switch (node.operator) {
        case "+":
          result = node.left.value + node.right.value;
          break
        case "-":
          result = node.left.value - node.right.value;
          break;
        case "*":
          result =  node.left.value * node.right.value;
          break;
        case "/":
          result =  node.left.value / node.right.value;
          break;
        case "**":
          let i = node.right.value;
          while (--i) {
            result = result || node.left.value;
            result =  result * node.left.value;
          }
          break;
        default:
      }
    }

    // 如果上面的運算有結果的話
    if (result !== undefined) {
      // 把表示式節點替換成number字面量
      path.replaceWith(t.numericLiteral(result));
    }
  }
};

module.exports = function (babel) {
  return {
    visitor
  };
}
複製程式碼

外掛寫好了,我們執行下外掛試試

const babel = require("babel-core");

const result = babel.transform("const result = 1 + 2;",{
  plugins:[
    require("./index")
  ]
});

console.log(result.code); // const result = 3;
複製程式碼

與預期一致,那麼轉換 const result = 1 + 2 + 3 + 4 + 5;呢?

結果是: const result = 3 + 3 + 4 + 5;

這就奇怪了,為什麼只計算了1 + 2之後,就沒有繼續往下運算了?

我們看一下這個表示式的AST樹

2

你會發現Babel解析成表示式裡面再巢狀表示式。

表示式( 表示式( 表示式( 表示式(1 + 2) + 3) + 4) + 5)
複製程式碼

而我們的判斷條件並不符合所有的,只符合1 + 2

    // 判斷表示式兩邊,是否都是數字
    if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {}
複製程式碼

那麼我們得改一改

第一次計算1 + 2之後,我們會得到這樣的表示式

表示式( 表示式( 表示式(3 + 3) + 4) + 5)
複製程式碼

其中 3 + 3又符合了我們的條件, 我們通過向上遞迴的方式遍歷父級節點

又轉換成這樣:

表示式( 表示式(6 + 4) + 5)
表示式(10 + 5)
15
複製程式碼
    // 如果上面的運算有結果的話
    if (result !== undefined) {
      // 把表示式節點替換成number字面量
      path.replaceWith(t.numericLiteral(result));

      let parentPath = path.parentPath;

      // 向上遍歷父級節點
      parentPath && visitor.BinaryExpression.call(this, parentPath);
    }
複製程式碼

到這裡,我們就得出了結果 const result = 15;

那麼其他運算呢:

const result = 100 + 10 - 50 >>> const result = 60;

const result = (100 / 2) + 50 >>> const result = 100;

const result = (((100 / 2) + 50 * 2) / 50) ** 2 >>> const result = 9;

完結

到這裡,已經向你大概的講解了,如何編寫一個Babel外掛,再也不怕面試官問我答不出什麼了哈...

你以為這就完了嗎?

並沒有

如果轉換這樣呢: const result = 0.1 + 0.2;

預期肯定是0.3, 但是實際上,Javascript有浮點計算誤差,得出的結果是0.30000000000000004

那是不是這個外掛就沒鳥用?

這就需要你去矯正浮點運算誤差了,可以使用Big.js;

比如: result = node.left.value + node.right.value; 改成 result = +new Big(node.left.value).plus(node.right.value);

你以為完了嗎? 這個外掛還可以做很多

比如: Math.PI * 2 >>> 6.283185307179586

比如: Math.pow(2, 2) >>> 4

...

...

最後上專案地址: github.com/axetroy/bab…

相關文章