從源碼看Babel是如何編譯Async和Generator函數的

某次面試場景:vue

面試官:你知道 async/await 嗎?webpack

我:有所瞭解(心中竊喜,看來下面要問我事件循環方面的東西了,立刻給你倒着背出來,穩得很)web

面試官:那請你說下 Bable 是如何處理 async/await 的? 或者直接描述一下相關 polyfill 的原理面試

我:。。。(怎麼不按套路出牌?)算法

我確實不知道這個東西,但爲了不尷尬,我只能秉持着雖然我不知道你說的這個東西但氣勢不能弱了必定要把你唬住的心理戰術,利用本身所知道的東西,進行現場算命推測,聲情並茂地介紹了一波 異步函數隊列化執行的模式,然而遺憾的是,我雖說得吐沫橫飛,但終究沒猜對promise

最近閒着沒事,因而抽時間看了一下babel

polyfill 後的代碼

既然想知道其原理,那麼天然是要看下 polyfill後的代碼的,直接到 Babel官網的REPL在線編輯器上,配置好 presetsplugins後,輸入你想要轉化的代碼,babel自動就會給你輸出轉化後的代碼了app

如下述代碼爲例:異步

async function test1 () {
  console.log(111)
  await a()
  console.log(222)
  await b()
  console.log(3)
}
複製代碼

babel輸出的代碼是:async

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));

function test1() {
  return _test.apply(this, arguments);
}

function _test() {
  _test = (0, _asyncToGenerator2.default)(
  /*#__PURE__*/
  _regenerator.default.mark(function _callee() {
    return _regenerator.default.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            console.log(111);
            _context.next = 3;
            return a();

          case 3:
            console.log(222);
            _context.next = 6;
            return b();

          case 6:
            console.log(3);

          case 7:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return _test.apply(this, arguments);
}
複製代碼

很明顯,_test函數中 while(1)方法體的內容,是須要首先注意的代碼

能夠看出來,babel把原代碼進行了一次分割,按照 await爲界限,將 async函數中的代碼分割到了 switch的每一個 case中(爲了表述方便,下文將此 case代碼塊中的內容稱做 await代碼塊), switch的條件是 _context.prev = _context.next,與 _context.next緊密相關,而 _context.next這個變量,會在每一個非 case end中被賦值,值就是原代碼中被分割後的下一個將要執行的 await代碼塊的內容,當原代碼中的全部 await被執行完畢後,會進入 case end邏輯,執行 return _context.stop(),表明 async函數已經執行完畢

但這只是最基本的,代碼究竟是怎麼串連起來的,還要繼續往外看

下文講解的源代碼版本:"@babel/runtime": "^7.8.4"

流程串連

首先,須要看下 _interopRequireDefault這個方法:

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : {
    "default": obj
  };
}

module.exports = _interopRequireDefault;
複製代碼

代碼很簡單,若是參數 obj上存在 __esModule這個屬性,則直接返回 obj,不然返回一個屬性 defaultobj的對象,其實這個主要就是爲了兼容 ESModuleCommonJS這兩種導入導出規範,保證當前的引用必定存在一個 default 屬性,不然沒有則爲其加一個 default屬性,這樣便不會出現模塊的 defaultundefined的狀況了,就是一個簡單的工具方法

而後繼續看 _regeneratorwhile(1)這個循環體所在的函數,做爲 _regenerator.default.wrap方法的參數被執行,_regenerator是從 @babel/runtime/regenerator引入的,進入 @babel/runtime/regenerator文件, 裏面只有一行代碼 :module.exports = require("regenerator-runtime");,因此最終應該是 regenerator-runtime庫,直接找 wrap方法

function wrap(innerFn, outerFn, self, tryLocsList) {
  // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator.
  var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator;
  var generator = Object.create(protoGenerator.prototype);
  var context = new Context(tryLocsList || []);

  // The ._invoke method unifies the implementations of the .next,
  // .throw, and .return methods.
  generator._invoke = makeInvokeMethod(innerFn, self, context);

  return generator;
}
複製代碼

innerFn_callee$outerFn_calleeouterFn.prototype也就是 _callee.prototype_callee也是一個函數,可是通過了 _regenerator.default.mark 這個方法的處理,看下 mark方法

exports.mark = function(genFun) {
  if (Object.setPrototypeOf) {
    Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);
  } else {
    genFun.__proto__ = GeneratorFunctionPrototype;
    if (!(toStringTagSymbol in genFun)) {
      genFun[toStringTagSymbol] = "GeneratorFunction";
    }
  }
  genFun.prototype = Object.create(Gp);
  return genFun;
};
複製代碼

主要就是爲了構造原型鏈,GeneratorFunctionPrototype以及 Gp又是什麼呢?

function Generator() {}
function GeneratorFunction() {}
function GeneratorFunctionPrototype() {}
// ...
var IteratorPrototype = {};
//...
var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype);
複製代碼

仍是構建原型鏈,最終以下:

因此,回到上面的 wrap方法,protoGenerator就是 outerFn,也就是_calleegenerator 的原型鏈指向 protoGenerator.prototype 這裏有個 context實例,由 Context構造而來在:

function Context(tryLocsList) {
  // The root entry object (effectively a try statement without a catch
  // or a finally block) gives us a place to store values thrown from
  // locations where there is no enclosing try statement.
  this.tryEntries = [{ tryLoc: "root" }];
  tryLocsList.forEach(pushTryEntry, this);
  this.reset(true);
}
複製代碼

主要看下 reset方法:

//...
Context.prototype = {
  constructor: Context,
  reset: function(skipTempReset) {
    this.prev = 0;
    this.next = 0;
    // Resetting context._sent for legacy support of Babel's
    // function.sent implementation.
    this.sent = this._sent = undefined;
    this.done = false;
    this.delegate = null;

    this.method = "next";
    this.arg = undefined;
    //...
  },
  //...
}
複製代碼

很明顯,reset方法的做用就和其屬性名同樣,是爲了初始化一些屬性,主要的屬性有 this.prevthis.next,用於交替記錄當前執行到哪些代碼塊了,this.done,用於標識當前代碼塊是否執行完畢,先不細說,後面會提到

而後 generator上掛載了一個 _invoke方法

// The ._invoke method unifies the implementations of the .next,
// .throw, and .return methods.
generator._invoke = makeInvokeMethod(innerFn, self, context);
複製代碼

看下 makeInvokeMethod的代碼:

function makeInvokeMethod(innerFn, self, context) {
  var state = GenStateSuspendedStart;
  return function invoke(method, arg) {
    //...
  }
}
複製代碼

粗略來看,此方法又返回了一個方法,至於方法體裏是什麼,暫時先無論,繼續往下看

_regenerator.default.mark(function _callee() {//...})做爲 _asyncToGenerator2.default方法的參數執行,因此繼續看 _asyncToGenerator2

function _asyncToGenerator(fn) {
  return function () {
    var self = this,
        args = arguments;
    return new Promise(function (resolve, reject) {
      var gen = fn.apply(self, args);

      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }

      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }

      _next(undefined);
    });
  };
}
複製代碼

_asyncToGenerator一樣返回了一個函數,這個函數內部又返回了一個 Promise,這對應着 async函數也是返回一個 promise, 經過_next調用 asyncGeneratorStep

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }

  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw);
  }
}
複製代碼

參數 gen其實就是上面提到過的 generator,正常狀況下,key"next"gen[key](arg); 至關於 generator.next(arg)generator上哪來的 next屬性呢?實際上是經過原型鏈找到 Gp,在Gp上就存在 next這個屬性:

// Helper for defining the .next, .throw, and .return methods of the
// Iterator interface in terms of a single ._invoke method.
function defineIteratorMethods(prototype) {
  ["next", "throw", "return"].forEach(function(method) {
    prototype[method] = function(arg) {
      return this._invoke(method, arg);
    };
  });
}
//...
defineIteratorMethods(Gp);
複製代碼

這個的 this._invoke(method, arg);,其實就是 generator._invoke("next", arg)

因此,如今再來看一下 makeInvokeMethod方法返回的 invoke方法,按照正常邏輯會走這一段代碼:

function tryCatch(fn, obj, arg) {
  try {
    return { type: "normal", arg: fn.call(obj, arg) };
  } catch (err) {
    return { type: "throw", arg: err };
  }
}
//...
var record = tryCatch(innerFn, self, context);
if (record.type === "normal") {
  // If an exception is thrown from innerFn, we leave state ===
  // GenStateExecuting and loop back for another invocation.
  state = context.done
    ? GenStateCompleted
    : GenStateSuspendedYield;

  if (record.arg === ContinueSentinel) {
    continue;
  }
  return {
    value: record.arg,
    done: context.done
  };
}
複製代碼

執行 tryCatch方法,返回了一個存在兩個屬性 valuedone的對象,其中 tryCatch的第一個參數 fn,就是包含 while(1)代碼段的 _callee$方法,這樣,整個流程就串起來了

流程解析

while(1)的循環體中,_context參數就是 Context的實例,上面提到過,_context上的 prevnext屬性都被初始化爲 0,因此會進入 case 0這個代碼塊,執行第一塊 await代碼塊,獲得info結果,判斷 info.done的值

if (info.done) {
  resolve(value);
} else {
  Promise.resolve(value).then(_next, _throw);
}
複製代碼

保證原async函數中全部 await代碼體所有執行完畢的邏輯就在此處

若是 info.done不爲 true,說明 原async函數中await代碼體尚未所有執行完畢,進入 else語句,利用 Promise.resolve來等待當前的 await代碼塊的 promise狀態改變,而後調用 then方法,經過執行 _next方法來調用 asyncGeneratorStep,繼續執行 _callee$,再次走 switch代碼段,根據更新後的 _context_prev來指示進入下一個 case,以此循環,當全部的 await代碼段執行完畢後,會進入 case 'end',執行 _context.stop();這個東西

Context.prototype = {
  constructor: Context,
  //...
  stop: function() {
    this.done = true;

    var rootEntry = this.tryEntries[0];
    var rootRecord = rootEntry.completion;
    if (rootRecord.type === "throw") {
      throw rootRecord.arg;
    }
    return this.rval;
  },
  //...
}
複製代碼

stop方法中,主要就是設置 this.donetrue,標識當前異步代碼段已經執行完畢,當下次再執行 asyncGeneratorStep的時候,進入:

if (info.done) {
  resolve(value);
}
複製代碼

再也不繼續調用 _next,流程結束

其實當時面試的時候,面試官問我 async/await的實現原理,我第一反應就是 Promise,但緊接着我又想到 Promise屬於 ES6polyfill這個東西最起碼也得是 ES5啊,因此我又放棄了這個想法,萬萬沒想到,還能夠雙層 polyfill

簡易版實現

經過上述分析可知,Babel對於 async/awaitpolyfill其實主要就是 Promise + 自調用函數,固然,前提是須要經過字符串解析器,將 async函數的按照 await爲分割點進行切分,這個字符串解析器涉及到的東西比較多,好比詞法分析、語法分析啦,通常都會藉助 @babel/parser/@babel/generator/@babel/traverse 系列,但這不是本文的重點,因此就不展開了

假設已經實現了一個解析器,可以將傳入的 async函數按照要求分割成幾部分

好比,對於如下源碼:

// wait() 是一個返回 promise 的函數
async function test1 () {
  console.log(111)
  await wait(500)
  console.log(222)
  await wait(1000)
  console.log(333)
}
複製代碼

將被轉化爲:

function test1 () {
  this.prev = 0
  return new Promise(resolve => {
    function loop(value, _next) {
      return Promise.resolve(value).then(_next)
    }
    function fn1 () {
      switch (this.prev) {
        case 0:
          console.log(111);
          this.prev = 3;
          return loop(wait(500), fn1);
        case 3:
          console.log(222);
          this.prev = 6;
          return loop(wait(1000), fn1);
        case 6:
          console.log(333);
          return resolve()
      }
    }
    fn1(resolve)
  })
}
複製代碼

固然,這只是簡易實現,不少東西都沒有考慮到,好比 await返回值啊,函數返回值啊等,只是爲了體現其原理

for 循環?

當時面試的時候,當我口若懸河地說完了 異步函數隊列化執行的模式 這個概念後,面試官可能沒想到我竟然在明知道本身是在猜的狀況還能心態這麼好地說了那麼多,沉默了片刻後,彷佛是想打壓一下我囂張的氣焰,又問,若是是 for循環呢,怎麼處理?

相似於如下代碼:

async function fn1 () {
  for (let i = 0; i < 10; i++) {
    await wait(i * 100)
  }
}
複製代碼

當時我其實已經知道猜錯了,但既然猜了那就猜到底,本身裝的逼不管如何也要圓回來啊,因而繼續用這個概念強行解釋了一通

實際上當時我對於 for循環的這個處理,思路上是對的,就是將 for循環拆解,拿到 單次表達式;條件表達式;末尾循環體 這個三個表達式,而後不斷改變 條件表達式,直到觸發末尾循環體,babel的處理結果以下:

// 只看主體代碼
switch (_context.prev = _context.next) {
  case 0:
    i = 0;

  case 1:
    if (!(i < 10)) {
      _context.next = 7;
      break;
    }

    _context.next = 4;
    return wait(i * 100);

  case 4:
    i++;
    _context.next = 1;
    break;

  case 7:
  case "end":
    return _context.stop();
}
複製代碼

這就揭示了 async/await函數的一個特性,那就是它具有暫停 for循環的能力,即對 for循環有效

Generator?

既然看完了 async/await的實現,那麼順便看下 Generator 對於下述代碼:

function* generatorFn() {
  console.log(111)
  yield wait(500)
  console.log(222)
  yield wait(1000)
  console.log(333)
}
複製代碼

Babel將其轉化爲:

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));

var _marked =
/*#__PURE__*/
_regenerator.default.mark(generatorFn);

function generatorFn() {
  return _regenerator.default.wrap(function generatorFn$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          console.log(111);
          _context.next = 3;
          return wait(500);

        case 3:
          console.log(222);
          _context.next = 6;
          return wait(1000);

        case 6:
          console.log(333);

        case 7:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}
複製代碼

這套路跟 async/await同樣啊,也是把原代碼進行切分,只不過Generator是按照 yield關鍵字切分的,最主要的區別是,轉化後的代碼相對於 async/await的來講,少了 _asyncToGenerator2這個方法的調用,而這個方法實際上是爲了自調用執行使用的,這同時也是 async/awaitGenerator的區別所在

async函數中,只要await後面的表達式返回的值是一個非Promise或者fulfilled態的 Promise,那麼async函數就會自動繼續往下執行,這在 polyfill中的表現就是一個自調用方法

至於 Generator函數想要在遇到 yield以後繼續執行,就必需要在外部手動調用 next方法,而調用的這個next,實際上在 async/awaitpolyfill中就是由 _asyncToGenerator2來自動調用的

除此以外,由於是手動調用,若是你不額外增長對異步 promise的處理,那麼 Generator自己是不會等待 promise狀態變化的,之因此說 async/awaitGenerator 函數的語法糖,部分緣由就在於 async/await相比於 Generator來講,已經內置了對異步 promise的處理

小結

最近參加了幾場面試,發現面試官們都很喜歡問你有哪些亮點,不論是業務層面仍是技術層面,並會按照你給出的答案深刻下去,看看你這個亮點到底有多亮

一個追問你亮點的面試官,實際上是比較願意給你機會的,技術的範圍太廣,可能他問的你剛好不熟悉,這是很常見的事情,好比你熟悉 vue,他團隊內用的都是 React,他追着你問 React可能很難問出結果來,另一方面,你也沒法保證在每場面試中都保持最佳狀態,萬一你跟面試官根本不在同一個頻道上,大家之間相互聽不懂對方在說什麼,還怎麼繼續?因此把選擇權交給你,給你機會讓你本身選,那麼這就引出另一個問題,若是你真的沒作過什麼有亮點的事情怎麼辦?給你機會你都抓不住,這可怪不到別人了

因此,若是你有一個較高的追求,那麼在平時的工做中,哪怕是每天寫業務代碼,你也要有本身的思考,這個組件可不能夠換一種寫法,那個需求是否是能夠簡化一下,項目裏的webpack需不須要升級到最新版,這個問題可不能夠造個輪子來一勞永逸地搞定它?

無關問題大小,均可以引起思考,實際上,通常狀況下也不太可能有什麼大問題等着你去解決,大部分狀況下都是小問題,但問題再小,解決得多了那也是一種可觀的積累,經過這種積累,在團隊內部,你就有了能夠拿出來講的輸出貢獻,離開了團隊,你也能以此抓住面試官給你的機會

有時候,這種亮點比你背面試題刷算法還好用,畢竟,面試題或者算法題會就是會,不會就是不會,可是亮點這種東西可沒有標準答案,能說的可多了去了

相關文章
相關標籤/搜索