Promise 向左,Async/Await 向右?
1. 前言
從事前端開發(fā)至今,異步問題經(jīng)歷了 Callback Hell 的絕望,Promise/Deffered 的規(guī)范混戰(zhàn),到 Generator 的所向披靡,到如今 Async/Await 為大眾所接受,這其中 Promise 和 Async/Await 依然活躍代碼中,對(duì)他們的認(rèn)識(shí)和評(píng)價(jià)也經(jīng)歷多次反轉(zhuǎn),也有各自的擁躉,形成了一直延續(xù)至今的愛恨情仇,其背后的思考和啟發(fā),依舊值得我們深思。
預(yù)先聲明:
本文的目標(biāo)并不是引發(fā)大家的論戰(zhàn),也不想去推崇其中任何一種方式來作為前端異步的唯一最佳實(shí)踐,想在介紹下 Promise 和 Async/Await 知識(shí)和背后的趣事的基礎(chǔ)上,探究下這些爭(zhēng)議下隱藏的共識(shí)。
2. Promise
Promise 是異步編程的一種解決方案,相對(duì)于傳統(tǒng)的回調(diào)地獄更加合理和強(qiáng)大。
所謂 Promise,簡(jiǎn)單來說就是一個(gè)容器,里面存儲(chǔ)個(gè)未來才會(huì)結(jié)束的時(shí)間的結(jié)果。從語法上說,Promise 是一個(gè)對(duì)象,從它可以獲取異步操作的消息。Promise 提供統(tǒng)一的 API,各種異步操作都可以用同樣的方法進(jìn)行處理。其內(nèi)部狀態(tài)如下:
狀態(tài)之間的流轉(zhuǎn)是不可逆的,代碼書寫如下:
function httpPromise(): Promise<{ success: boolean; data: any }> {
return new Promise((resolve, reject) => {
try {
setTimeout(() => {
resolve({ success: true, data: {} });
}, 1000);
} catch (error) {
reject(error);
}
});
}
httpPromise().then((res) => {}).catch((error) => {}).finally(() => {});
從語法角度上看,更加容易理解,但是美中不足的就是不夠簡(jiǎn)潔,無法斷點(diǎn),冗余的匿名函數(shù)。
2.1. Promise 是如何實(shí)現(xiàn)的?
在剛?cè)胄械臅r(shí)候也去研究過《如何實(shí)現(xiàn)一個(gè) Promise》這個(gè)課題,嘗試寫了下如下的代碼。
class promise {
constructor(handler) {
this.resolveHandler = null;
this.rejectedHandler = null;
setTimeout(() => {
handler(this.resolveHandler, this.rejectedHandler);
}, 0);
}
then(resolve, reject) {
this.resolveHandler = resolve;
this.rejectedHandler = reject;
returnthis;
}
}
function getPromise() {
return new promise((resolve, reject) => {
setTimeout(() => {
resolve(20);
}, 1000);
});
}
getPromise().then((res) => {
console.log(res);
}, (error) => {
console.log(error);
});
雖然羞恥,但是不得不說當(dāng)時(shí)還是挺滿足的,后面發(fā)現(xiàn)無法解決異步注冊(cè)的問題。
const promise1 = getPromise();
setTimeout(() => {
promise1.then((data) => {
console.log(data);
}).catch((error) => {
console.error(error);
});
}, 0);
function getFPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(20), 1000);
});
}
// 執(zhí)行情況 報(bào)錯(cuò)
// Uncaught TypeError: promise1.then(...).catch is not a function
// Uncaught TypeError: resolve is not a function
// vs 官方Promise實(shí)現(xiàn)
const promise2 = getFPromise();
setTimeout(() => {
promise2.then((data) => {
console.log(data);
}).catch((error) => {
console.error(error);
});
}, 0);
// 執(zhí)行情況,符合預(yù)期
// 20對(duì)于這一部分有興趣的同學(xué)可以自行查找標(biāo)準(zhǔn)版的實(shí)現(xiàn)方案,不過在這個(gè)探索過程中確實(shí)勾起對(duì)基礎(chǔ)知識(shí)的興趣,這也是本文去挖掘這部分知識(shí)的初衷。
接下來看看 Async/Await 吧。
3. Async/Await
Async/Await 并不是什么新鮮的概念,事實(shí)的確如此。
早在 2012 年微軟的 C# 語言發(fā)布 5.0 版本時(shí),就正式推出了 Async/Await 的概念,隨后在 Python 和 Scala 中也相繼出現(xiàn)了 Async/Await 的身影。再之后,才是我們今天討論的主角,ES 2016 中正式提出了 Async/Await 規(guī)范。
以下是一個(gè)在 C# 中使用 Async/Await 的示例代碼:
public async Task<int> SumPageSizesAsync(IList uris )
{
int total = 0;
foreach (var uri in uris) {
statusText.Text = string.Format("Found {0} bytes ...", total);
var data = await new WebClient().DownloadDataTaskAsync(uri);
total += data.Length;
}
statusText.Text = string.Format("Found {0} bytes total", total);
return total;
}
再看看在 JavaScript 中的使用方法:
async function httpRequest(value) {
const res = await axios.post({ ...value });
return res;
}
好的設(shè)計(jì)總是會(huì)想借鑒的,不寒磣。
其實(shí)在前端領(lǐng)域,也有不少類 Async/Await 的實(shí)現(xiàn),其中不得不提到的就是知名網(wǎng)紅之一的老趙寫的?wind.js,站在今天的角度看,windjs 的設(shè)計(jì)和實(shí)現(xiàn)不可謂不超前。
3.1. Async/Await 是如何實(shí)現(xiàn)的?
ES2017 標(biāo)準(zhǔn)引入了 async 函數(shù),使得異步操作變得更加方便。
這里引用阮一峰老師的描述:
async 函數(shù)是什么?一句話,它就是 Generator 函數(shù)的語法糖。
前文有一個(gè) Generator 函數(shù),依次讀取兩個(gè)文件。
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
上面代碼的函數(shù)?gen?可以寫成?async?函數(shù),就是下面這樣。
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
一比較就會(huì)發(fā)現(xiàn),async?函數(shù)就是將 Generator 函數(shù)的星號(hào)(*)替換成?async,將?yield?替換成?await,僅此而已。
相對(duì)于 Generator 的改進(jìn)主要集中集中在:
內(nèi)置執(zhí)行器
更好的語義化
Promise 的返回值
到這里大家會(huì)發(fā)現(xiàn),Async/Await 本質(zhì)也是 Promise 的語法糖:Async 函數(shù)返回了 Promise 對(duì)象。
來看下實(shí)際 Babel 轉(zhuǎn)化的代碼,方便大家理解下
async function test() {
const img = await fetch('tiger.jpg');
}
// babel 轉(zhuǎn)換后
'use strict';
var test = function() {
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee() {
var img;
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case0:
_context.next = 2;
return fetch('tiger.jpg');
case2:
img = _context.sent;
case3:
case'end':
return _context.stop();
}
}
}, _callee, this);
}));
return function test() {
return _ref.apply(this, arguments);
};
}();
function _asyncToGenerator(fn) {
return function() {
var gen = fn.apply(this, arguments);
return new Promise(function(resolve, reject) {
function step(key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
return Promise.resolve(value).then(function(value) {
step("next", value);
}, function(err) {
step("throw", err);
});
}
}
return step("next");
});
};
}
不難看出最終還是轉(zhuǎn)換成基于 Promise 的調(diào)用,但是本來的三行代碼被轉(zhuǎn)換成 52 行代碼,在一些場(chǎng)景下就帶來了成本。
例如 Vue3 并沒有采用?.(可選鏈?zhǔn)讲僮鞣?hào))的原因:

雖然使用?很簡(jiǎn)潔,但是實(shí)際打包下反而更加冗余了,增加了包的體積,影響 Vue3 的加載速度,這也是 Async/Await 這類簡(jiǎn)潔語法的痛點(diǎn)。
暫時(shí)不考慮深層次的運(yùn)行性能,僅僅考慮代碼使用方式來看,Async/Await 是否完美無缺?
以 “請(qǐng)求 N 次重試” 的實(shí)現(xiàn)為例:
/**
* @description: 限定次數(shù)來進(jìn)行請(qǐng)求
* @example: 例如在5次內(nèi)獲取到結(jié)果
* @description: 核心要點(diǎn)是完成tyscript的類型推定,其次高階函數(shù)
* @param T 指定返回?cái)?shù)據(jù)類型,M指定參數(shù)類型
*/
export default function getLimitTimeRequest<T>(task: any, times: number) {
// 獲取axios的請(qǐng)求實(shí)例
let timeCount = 0;
async function execTask(resolve, reject, ...params: any[]): Promise<void> {
if (timeCount > times) {
reject(newError('重試請(qǐng)求失敗'));
}
try {
const data: T = await task(...params);
if (data) {
resolve(data);
} else {
timeCount++;
execTask(resolve, reject, params);
}
} catch (error) {
timeCount++;
execTask(resolve, reject, params);
}
}
return function <M>(...params: M[]): Promise<T> {
return new Promise((resolve, reject) => {
execTask(resolve, reject, ...params);
});
};
}
常見的實(shí)現(xiàn)思路是將 Promise 的 Resolve、Reject 的句柄傳遞到迭代函數(shù)中,來控制 Promise 的內(nèi)部狀態(tài)轉(zhuǎn)化,那如果用 Async/Await 如何做?很明顯并不好做,暴露了它的一些不足:
缺少復(fù)雜的控制流程,如 always、progress、pause、resume 等
內(nèi)部狀態(tài)無法控制,錯(cuò)誤捕獲嚴(yán)重依賴 try/catch
缺少中斷的方法,無法 abort
當(dāng)然,站在 EMCA 規(guī)范的角度來看,有些需求可能比較少見,但是如果納入規(guī)范中,也可以減少前端程序員在挑選異步流程控制庫時(shí)的糾結(jié)了。
4. 總結(jié)
針對(duì)前端異步的處理方案,Promise 和 Async/Await 都是優(yōu)秀的處理方案,但是美中不足的是有一定的不足,隨著前端工程化的深入,一定會(huì)有更好的方案來迎合解決這些問題,大家不要失望,未來還是可期的。
從 Promise 和 Async/Await 的演進(jìn)和糾結(jié)中,大家實(shí)際能夠感到前端人對(duì) JavaScript 世界的辛苦耕作和奇思妙想,這種思維和方式也可以沉淀到我們?nèi)粘5男枨箝_發(fā)中去,善于求索,辯證的去使用它們,追求更加極致的方案。


