某次面試場景:vue
面試官:你知道 async/await
嗎?webpack
我:有所瞭解(心中竊喜,看來下面要問我事件循環方面的東西了,立刻給你倒着背出來,穩得很)web
面試官:那請你說下 Bable
是如何處理 async/await
的? 或者直接描述一下相關 polyfill
的原理面試
我:。。。(怎麼不按套路出牌?)算法
我確實不知道這個東西,但爲了不尷尬,我只能秉持着雖然我不知道你說的這個東西但氣勢不能弱了必定要把你唬住的心理戰術,利用本身所知道的東西,進行現場算命推測,聲情並茂地介紹了一波 異步函數隊列化執行的模式,然而遺憾的是,我雖說得吐沫橫飛,但終究沒猜對promise
最近閒着沒事,因而抽時間看了一下babel
既然想知道其原理,那麼天然是要看下 polyfill
後的代碼的,直接到 Babel官網的REPL在線編輯器上,配置好 presets
和 plugins
後,輸入你想要轉化的代碼,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
,不然返回一個屬性 default
爲 obj
的對象,其實這個主要就是爲了兼容 ESModule
和 CommonJS
這兩種導入導出規範,保證當前的引用必定存在一個 default
屬性,不然沒有則爲其加一個 default
屬性,這樣便不會出現模塊的 default
爲 undefined
的狀況了,就是一個簡單的工具方法
而後繼續看 _regenerator
,while(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
是 _callee
, outerFn.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
,也就是_callee
,generator
的原型鏈指向 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.prev
、this.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
方法,返回了一個存在兩個屬性 value
、done
的對象,其中 tryCatch
的第一個參數 fn
,就是包含 while(1)
代碼段的 _callee$
方法,這樣,整個流程就串起來了
在 while(1)
的循環體中,_context
參數就是 Context
的實例,上面提到過,_context
上的 prev
和 next
屬性都被初始化爲 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.done
爲 true
,標識當前異步代碼段已經執行完畢,當下次再執行 asyncGeneratorStep
的時候,進入:
if (info.done) {
resolve(value);
}
複製代碼
再也不繼續調用 _next
,流程結束
其實當時面試的時候,面試官問我 async/await
的實現原理,我第一反應就是 Promise
,但緊接着我又想到 Promise
屬於 ES6
,polyfill
這個東西最起碼也得是 ES5
啊,因此我又放棄了這個想法,萬萬沒想到,還能夠雙層 polyfill
經過上述分析可知,Babel
對於 async/await
的 polyfill
其實主要就是 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
循環呢,怎麼處理?
相似於如下代碼:
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
循環有效
既然看完了 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/await
和 Generator
的區別所在
async
函數中,只要await
後面的表達式返回的值是一個非Promise
或者fulfilled
態的 Promise
,那麼async
函數就會自動繼續往下執行,這在 polyfill
中的表現就是一個自調用方法
至於 Generator
函數想要在遇到 yield
以後繼續執行,就必需要在外部手動調用 next
方法,而調用的這個next
,實際上在 async/await
的 polyfill
中就是由 _asyncToGenerator2
來自動調用的
除此以外,由於是手動調用,若是你不額外增長對異步 promise
的處理,那麼 Generator
自己是不會等待 promise
狀態變化的,之因此說 async/await
是 Generator
函數的語法糖,部分緣由就在於 async/await
相比於 Generator
來講,已經內置了對異步 promise
的處理
最近參加了幾場面試,發現面試官們都很喜歡問你有哪些亮點,不論是業務層面仍是技術層面,並會按照你給出的答案深刻下去,看看你這個亮點到底有多亮
一個追問你亮點的面試官,實際上是比較願意給你機會的,技術的範圍太廣,可能他問的你剛好不熟悉,這是很常見的事情,好比你熟悉 vue
,他團隊內用的都是 React
,他追着你問 React
可能很難問出結果來,另一方面,你也沒法保證在每場面試中都保持最佳狀態,萬一你跟面試官根本不在同一個頻道上,大家之間相互聽不懂對方在說什麼,還怎麼繼續?因此把選擇權交給你,給你機會讓你本身選,那麼這就引出另一個問題,若是你真的沒作過什麼有亮點的事情怎麼辦?給你機會你都抓不住,這可怪不到別人了
因此,若是你有一個較高的追求,那麼在平時的工做中,哪怕是每天寫業務代碼,你也要有本身的思考,這個組件可不能夠換一種寫法,那個需求是否是能夠簡化一下,項目裏的webpack
需不須要升級到最新版,這個問題可不能夠造個輪子來一勞永逸地搞定它?
無關問題大小,均可以引起思考,實際上,通常狀況下也不太可能有什麼大問題等着你去解決,大部分狀況下都是小問題,但問題再小,解決得多了那也是一種可觀的積累,經過這種積累,在團隊內部,你就有了能夠拿出來講的輸出貢獻,離開了團隊,你也能以此抓住面試官給你的機會
有時候,這種亮點比你背面試題刷算法還好用,畢竟,面試題或者算法題會就是會,不會就是不會,可是亮點這種東西可沒有標準答案,能說的可多了去了