1. 程式人生 > 實用技巧 >面試官: 說說你對async的理解

面試官: 說說你對async的理解


大家好,我是小雨小雨,致力於分享有趣的、實用的技術文章。




內容分為翻譯和原創,如果有問題,歡迎隨時評論或私信,希望和大家一起進步。




分享不易,希望能夠得到大家的支援和關注。



TL;DR


async是generator和promise的語法糖,利用迭代器的狀態機和promise來進行自更新!


如果懶得往下看,可以看下這個極其簡易版本的實現方式:


// 複製貼上即可直接執行
function stateMac (arr) {
let val;
return {
next(){
if ((val = arr.shift())) {
return {
value: val,
done: false
}
} else {
return {
done: true
}
}
}
}
}


function asyncFn(arr) {
const iterator = stateMac(arr);
function doSelf () {
const cur = iterator.next();
const value = cur.value;
if (cur.done) {
console.log('done');
return;
}
switch (true) {
case value.then && value.toString() === '[object Promise]':
value.then((result) => {
console.log(result);
doSelf();
})
break;
case typeof value === 'function':
value();
doSelf();
break;
default:
console.log(value);
doSelf();
}
}
doSelf();
}

const mockAsync = [
1,
new Promise((res) => {
setTimeout(function () {
res('promise');
}, 3000);
}),
function () {
console.log('測試');
}
];
console.log('開始');
asyncFn(mockAsync);
console.log('結束');

前言


async & await 和我們的日常開發緊密相連,但是你真的瞭解其背後的原理嗎?



本文假設你對promise、generator有一定了解。



簡述promise


promise就是callback的另一種寫法,避免了毀掉地獄,從橫向改為縱向,大大提升了可讀性和美觀。


至於promise的實現,按照promise A+規範一點點寫就好了,完成後可以使用工具進行測試,確保你的寫的東西是符合規範的。


具體實現原理,市面上有各種各樣的寫法,我就不多此一舉了。


簡述generator


generator就不像promise那樣,他改變了函式的執行方式。可以理解為協程,就是說多個函式互相配合完成任務。類似於這個東西:


function generator() {
return {
_value: [1, 2, 3, 4],
next() {
return {
value: this._value.shift(),
done: !this._value.length
};
}
};
}
const it = generator();

console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());

這只是一個demo,僅供參考。


具體請參考MDN.


async & await


照我的理解,其實就是generator和promise相交的產物,被解析器識別,然後轉換成我們熟知的語法。


這次要做的就是去看編譯之後的結果是什麼樣的。


既然如此,我們就帶著問題去看,不然看起來也糟心不是~


async包裝的函式會返回一個什麼樣的promise?


// 原始碼:
async function fn() {}

fn();

// 編譯後變成了一大坨:

// generator的polyfill
require('regenerator-runtime/runtime');

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);
}
}

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);
});
};
}

function fn() {
return _fn.apply(this, arguments);
}

function _fn() {
_fn = _asyncToGenerator(
/*#__PURE__*/ regeneratorRuntime.mark(function _callee() {
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
case 0:
case 'end':
return _context.stop();
}
}
}, _callee);
})
);
return _fn.apply(this, arguments);
}

fn();

內容也不是很多,我們一點點來看:


generator包裝

fn內部呼叫的是_fn,一個私有方法,使用的apply繫結的this,並傳入了動態引數。


_fn內呼叫了_asyncToGenerator方法,由於js呼叫棧後進先出:



讀起來是這樣的:fn() => _asyncToGenerator => .mark()




執行是反過來的:.mark() => _asyncToGenerator => fn()



我們先往裡看,映入眼簾的是regeneratorRuntime.mark,該方法是generator的polyfill暴露的方法之一,我們去內部(require('regenerator-runtime/runtime'))簡單看下這個mark是用來幹什麼的。


// 立即執行函式,適配commonjs和瀏覽器
(function (exports) {
// 暴露mark方法
exports.mark = function (genFun) {
// 相容判斷__proto__,處理老舊環境
if (Object.setPrototypeOf) {
Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);
} else {
genFun.__proto__ = GeneratorFunctionPrototype;
// 設定Symbol.toStringTag,適配toString
if (!(toStringTagSymbol in genFun)) {
genFun[toStringTagSymbol] = 'GeneratorFunction';
}
}
// 設定原型
genFun.prototype = Object.create(Gp);
return genFun;
};
})(typeof module === 'Object' ? module.exports : {});

mark做了兩個操作,一個是設定genFun的__proto__,一個是設定prototype,可能有人會好奇:



__proto__不是物件上的嗎?prototype不是函式上的嗎?為啥兩個同時應用到一個上面了



這樣操作是沒問題的,genFun不僅是函式啊,函式還是物件,js中萬物皆物件哦。你想想是不是可以通過Function建構函式new出一個函式?


然後開始設定__proto__和prototype,在次之前,我們來簡單捋一下原型。


原型


下面是個人理解的一個說法,未查閱v8引擎,但是這樣是說得通的。如果有問題,歡迎指出,一起溝通,我也會及時修改,以免誤導他人!!!。



首先要知道這三個的概念:搞清物件的原型物件(proto)、建構函式的原型(prototype)、構造方法(constructor)。


方便記憶,只需要記住下面幾條即可:



  • prototype是建構函式(注意:建構函式也是物件嗷)上特有的屬性,代表建構函式的原型。舉個例子:


有一位小明同學(指代建構函式),他有自己的朋友圈子(指代prototype),通過小明可以找到小紅(建構函式.prototype.小紅),在通過小紅的朋友圈子(prototype)還能找到小藍,直到有一個人(指代null),孑然一身、無慾無求,莫得朋友。


上面這個關係鏈就可以理解為原型鏈。



  • __proto__是每一個物件上特有的屬性,指向當前物件建構函式的prototype。再舉個例子:


小明家裡催的急,不就就生了個大胖小子(通過建構函式{小明}創造出物件{大胖小子}),可以說這個大胖小子一出生就被眾星捧月,小明的朋友們紛紛表示,以後孩子有啥事需要幫忙找我就成。這就指代物件上的__proto____proto__可以引用建構函式的任何關係。


所以說,程式碼源於生活~



  • constructor是啥呢,就是一個prototype上的屬性,表示這個朋友圈子是誰的,對於小明來說: 小明.prototype.constructor === 小明。所以,當我們進行繼成操作的時候,有必要修正一下constructor,不然朋友圈子就亂了~


  • js中函式和物件有點套娃的意思,萬物皆物件,物件又是從建構函式構造而來。對於小明來說,就是我生我生我~~



來看兩個判斷:


proto 指向構造當前物件的建構函式的prototype,由於萬物皆物件,物件又是通過建構函式構造而來。故Object通過Function構造而來,所以指向了Function.prototype


console.log(Object.__proto__ === Function.prototype); // => true

proto 指向構造當前物件的建構函式的prototype,由於萬物皆物件,物件又是通過建構函式構造而來。故Function通過Function構造而來,所以指向了Function.prototype


console.log(Function.__proto__ === Function.prototype); // => true

有興趣的朋友可以再看看這篇文章




然後,我們再來看看這張圖,跟著箭頭走一遍,是不是就很清晰了?



繼續generator包裝

mark方法會指定genFun的__proto__和prototype,完完全全替換了genFun的朋友圈以及創造genFun的建構函式的朋友圈,現在genFun就是Generator的克隆品了。


用來設定__proto__ 和 prototype的值,GeneratorFunctionPrototype,GP,我們也簡單過一下:



// 建立polyfill物件
var IteratorPrototype = {};
IteratorPrototype[iteratorSymbol] = function () {
return this;
};

// 原型相關操作
// 獲取物件的原型: __proto__
var getProto = Object.getPrototypeOf;

// 原生iterator原型
var NativeIteratorPrototype = getProto && getProto(getProto(values([])));
// IteratorPrototype設定為原生
if (
NativeIteratorPrototype &&
NativeIteratorPrototype !== Op &&
hasOwn.call(NativeIteratorPrototype, iteratorSymbol)
) {
// This environment has a native %IteratorPrototype%; use it instead
// of the polyfill.
IteratorPrototype = NativeIteratorPrototype;
}

// 創造原型
// Gp 為 迭代器原型
// IteratorPrototype作為原型物件
var Gp = (GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(
IteratorPrototype
));

// 更新建構函式和原型
GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;
GeneratorFunctionPrototype.constructor = GeneratorFunction;

// toString,呼叫Object.toString.call的時候會返回GeneratorFunction
GeneratorFunctionPrototype[
toStringTagSymbol
] = GeneratorFunction.displayName = 'GeneratorFunction';

最後再返回經過處理的genFun,然後再回到mark函式外~


_asyncToGenerator

_asyncToGenerator 接收mark處理過的結果:


// fn 為 generator 的克隆品
function _asyncToGenerator(fn) {
return function () {
var self = this,
args = arguments;
return new Promise(function (resolve, reject) {
// 呼叫_callee,先看下面,一會在回來哈~
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);
});
};
}

regeneratorRuntime.wrap

上面的_asyncToGenerator執行後,會執行mark返回的函式:


function _callee() {
return regeneratorRuntime.wrap(function _callee$(
_context
) {
// 這裡就是動態得了,也就是根據使用者寫的async函式,轉換的記過,由於我們是一個空函式,所以直接stop了
while (1) {
switch ((_context.prev = _context.next)) {
case 0:
case 'end':
return _context.stop();
}
}
},
_callee);
}

_callee會返回wrap處理後的結果,我們繼續看:


// innerFn是真正執行的函式,outerFn為被mark的函式
// self, tryLocsList未傳遞,為undefined
function wrap(innerFn, outerFn, self, tryLocsList) {
// If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator.
// outerFn 的原型已經被 mark重新設定,所以會包含generator相關原型
var protoGenerator =
outerFn && outerFn.prototype instanceof Generator
? outerFn
: Generator;

// 建立自定義原型的物件
var generator = Object.create(protoGenerator.prototype);

// context 例項是包含的 this.tryEntries 的
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;
}

其中有個new Context()的操作,用來重置並記錄迭代器的狀態,後面會用到。
之後給返回generator掛載一個_invoke方法,呼叫makeInvokeMethod,並傳入self(未傳遞該引數,為undefined)和context。


function makeInvokeMethod(innerFn, self, context) {
// state只有在該函式中備操作
var state = GenStateSuspendedStart; // GenStateSuspendedStart: 'suspendedStart'

// 作為外面的返回值
return function invoke(method, arg) {
// 這裡就是generator相關的一些操作了,用到的時候再說
};
}

利用閉包初始化state,並返回一個invoke函式,接受兩個引數,方法和值。先看到這,繼續往後看。


回到之前的_asyncToGenerator


// 返回帶有_invoke屬性的generator物件
var gen = fn.apply(self, args);

之後定義了一個next和throw方法,隨後直接呼叫_next開始執行:


function _next(value) {
asyncGeneratorStep(
gen, // 迭代器函式
resolve, // promise的resolve
reject, // promise的project
_next, // 當前函式
_throw, // 下面的_throw函式
'next', // method名
value // arg 引數值
);
}
function _throw(err) {
asyncGeneratorStep(
gen,
resolve,
reject,
_next,
_throw,
'throw',
err
);
}
_next(undefined);

其中都是用的asyncGeneratorStep,並傳遞了一些引數。


那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
resolve(value);
} else {
// 否則,繼續下次next呼叫,形成遞迴
Promise.resolve(value).then(_next, _throw);
}
}

程式碼很少,獲取即將要呼叫的方法名(key)並傳入引數,所以當前info即是:


var info = gen['next'](arg);

那next是哪來的那?就是之前mark操作中定義的,如果原生支援,就是用原生的迭代器提供的next,否則使用polyfill中定義的next。


還記得之前的makeInvokeMethod嗎?


它其實是用來定義標準化next、throw和return的:


function defineIteratorMethods(prototype) {
['next', 'throw', 'return'].forEach(function (method) {
prototype[method] = function (arg) {
return this._invoke(method, arg);
};
});
}
// Gp在之前的原型操作有用到
defineIteratorMethods(Gp);

然後當我們執行的時候,就會走到_invoke定義的invoke方法中:


function invoke(method, arg) {
// 狀態判斷,拋錯
if (state === GenStateExecuting) {
throw new Error('Generator is already running');
}

// 已完成,返回done狀態
if (state === GenStateCompleted) {
if (method === 'throw') {
throw arg;
}

// Be forgiving, per 25.3.3.3.3 of the spec:
// https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume
return doneResult();
}

// 這裡就是之前定義的Context例項,下面程式碼沒啥了,自己看吧
context.method = method;
context.arg = arg;

while (true) {
var delegate = context.delegate;
if (delegate) {
var delegateResult = maybeInvokeDelegate(delegate, context);
if (delegateResult) {
if (delegateResult === ContinueSentinel) continue;
return delegateResult;
}
}

if (context.method === 'next') {
// Setting context._sent for legacy support of Babel's
// function.sent implementation.
context.sent = context._sent = context.arg;
} else if (context.method === 'throw') {
if (state === GenStateSuspendedStart) {
state = GenStateCompleted;
throw context.arg;
}

context.dispatchException(context.arg);
} else if (context.method === 'return') {
context.abrupt('return', context.arg);
}

state = GenStateExecuting;

// innerFn就是while個迴圈了,使我們的程式碼主體
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
};
} else if (record.type === 'throw') {
state = GenStateCompleted;
// Dispatch the exception by looping back around to the
// context.dispatchException(context.arg) call above.
context.method = 'throw';
context.arg = record.arg;
}
}
};

在之後,就是我們熟悉的promise相關操作了,在判斷done是否為true,否則繼續執行,將_next和_throw作為resolve和reject傳入即可。


小結

可以看到,僅僅一個async其實做了不少工作。核心就是兩個,產出一個相容版本的generator和使用promise,回到這節的問題上,答案就是:


return new Promise(function (resolve, reject) {});

沒錯,就是返回一個Promise,內部會根據狀態及決定是否繼續執行下一個Promise.resolve().then()。


如果async函式內有很多其他操作的程式碼,那麼while會跟著變化,利用prev和next來管理執行順序。這裡就不具體分析了,自己寫個例子就明白了~


可以通過babel線上轉換,給自己一個具象的感知,更利於理解。


為什麼下面這種函式外的console不會等待,函式內的會等待?


async function fn() {
await (async () => {
await new Promise((r) => {
setTimeout(function () {
r();
}, 2000);
});
})();
console.log('你好');
}
fn();
console.log(123);

因為解析後的console.log(123); 是在整個語法糖之外啊,log 和 fn 是主協程式,fn內是輔協程。不相干的。


總結


有句話怎麼說來著,會者不難,難者不會。所以人人都是大牛,只是你還沒發力而已,哈哈~


筆者後來思考覺得這種寫法完全就是回撥函式的替代品,而且增加了空間,加深了呼叫堆疊,或許原生的寫法才是效率最高的吧。


不過,需要良好的編碼規範,算是一種折中的方式了。畢竟用這種方式來寫業務事半功倍~


對於本文觀點,完全是個人閱讀後的思考,如有錯誤,歡迎指正,我會及時更新,避免誤導他人。


拜了個拜~