簡介
很久以前,在Promise出現(xiàn)之前,編寫js的異步代碼是一件十分蛋疼的事情,一旦異步流程一復(fù)雜,回調(diào)地獄就會等著你,所以基于node.js的服務(wù)端編碼開發(fā)的體驗(yàn)非常差,項(xiàng)目的可維護(hù)性和可讀性也非常差,這也是最初node.js最為人所詬病的地方。
在現(xiàn)代的js項(xiàng)目中,回調(diào)函數(shù)已經(jīng)基本被Promise取代,而async/await的出現(xiàn)更是解決的promise在使用中會產(chǎn)生大量的膠水代碼,以及不同Promise之間共享數(shù)據(jù)困難的問題,這又大大提升的編寫異步流程的開發(fā)體驗(yàn),但是async/await本身是基于promise的,而在很多情況下,async/await并不能完全取代promise的位置,理解promise是編寫高質(zhì)量的異步代碼的基礎(chǔ)。下面我們就來探討一下在js中的異步流程控制的問題。如何在異步代碼中保證異步流程的執(zhí)行效率以及可讀性和可維護(hù)性。以下為一個(gè)例子:
如何制作一個(gè)披薩?
制作餅皮(dough)
制作醬汁(sauce)
品嘗一下醬汁,根據(jù)醬汁的口味決定放哪種芝士(cheese)
第一版實(shí)現(xiàn)
async function makePizza(sauceType = 'red') {
let dough = await makeDough();
let sauce = await makeSauce(sauceType);
let cheese = await grateCheese(sauce.determineCheese());
dough.add(sauce);
dough.add(cheese);
return dough;
}
基于async/await,看起來邏輯很清晰,但是它最大的問題是這不是一個(gè)最優(yōu)的異步流程控制。
第二版實(shí)現(xiàn)
// 原來的流程
|-------- dough --------> |-------- sauce --------> |-- cheese -->
// 改進(jìn)的流程
|-------- dough -------->
|-------- sauce --------> |-- cheese -->
async function makePizza(sauceType = 'red') {
let [ dough, sauce ] =
await Promise.all([ makeDough(), makeSauce(sauceType) ]);
let cheese = await grateCheese(sauce.determineCheese());
dough.add(sauce);
dough.add(cheese);
return dough;
}
效率比第一版就高了很多。使用promise.all并行執(zhí)行異步任務(wù),流程也比較清晰。
第三版實(shí)現(xiàn)
由于制作dough的時(shí)間是比較長的,我們的流程其實(shí)還有改進(jìn)的空間,如下。
// 上一步改進(jìn)的流程
|-------- dough -------->
|--- sauce ---> |-- cheese -->
// 進(jìn)一步改進(jìn)的流程
|--------- dough --------->
|---- sauce ----> |-- cheese -->
function makePizza(sauceType = 'red') {
let doughPromise = makeDough();
let saucePromise = makeSauce(sauceType);
let cheesePromise = saucePromise.then(sauce => {
return grateCheese(sauce.determineCheese());
});
return Promise.all([ doughPromise, saucePromise, cheesePromise ])
.then(([ dough, sauce, cheese ]) => {
dough.add(sauce);
dough.add(cheese);
return dough;
});
}
現(xiàn)在的異步流程應(yīng)該是最優(yōu)的了,但是問題來了,這樣的流程用async/await表示的話就有點(diǎn)麻煩了,我們這里先用promise實(shí)現(xiàn)了這個(gè)最優(yōu)流程,可以看到promise的弊端很明顯,充滿了膠水代碼,我們再也不能像我們的第一版實(shí)現(xiàn)一樣,一眼就能看出我們這個(gè)函數(shù)的異步流程是怎么樣的了,可讀性下降了。
第四版實(shí)現(xiàn)
// 最優(yōu)流程
|--------- dough --------->
|---- sauce ----> |-- cheese -->
async function makePizza(sauceType = 'red') {
let doughPromise = makeDough();
let saucePromise = makeSauce(sauceType);
let sauce = await saucePromise;
let cheese = await grateCheese(sauce.determineCheese());
let dough = await doughPromise;
dough.add(sauce);
dough.add(cheese);
return dough;
}
這一版實(shí)現(xiàn),代碼看起來整潔了很多,但是它的異步流程更加不清晰了,通過提前創(chuàng)建promise然后在之后再進(jìn)行await,我們得到了整潔的代碼,但是卻使得流程更加不清晰,隱式的執(zhí)行promise然后await,讓await的語義變得很不明了。
第五版實(shí)現(xiàn)
async function makePizza(sauceType = 'red') {
let prepareDough = memoize(async () => makeDough());
let prepareSauce = memoize(async () => makeSauce(sauceType));
let prepareCheese = memoize(async () => {
return grateCheese((await prepareSauce()).determineCheese());
});
let [ dough, sauce, cheese ] =
await Promise.all([
prepareDough(), prepareSauce(), prepareCheese()
]);
dough.add(sauce);
dough.add(cheese);
return dough;
}
這個(gè)版本相比于上一個(gè)版本的區(qū)別主要是現(xiàn)在我們不再提前創(chuàng)建promise,而保存為一個(gè)生成函數(shù),并且使用memoize緩存結(jié)果,從而使得promise只需要resolve一次就可以緩存結(jié)果。這樣子的異步流程相比于上一個(gè)版本就清楚了很多,而代碼依然可以保持很好的可讀性。
總結(jié)
感覺Promise.all和await/async的結(jié)合依然不是很優(yōu)雅,總感覺有點(diǎn)難受,在未來,js的異步流程控制肯定還會有更優(yōu)雅的解決方案。