原文:https://yunchi.dev/posts/demystifying-async/
從 callback 到 promise 到 generator绅络,再到 asyc/await,Javascript 中的異步編程經(jīng)歷了一系列演變蠢甲。盡管每次變化都為那些深陷 Javascript 異步編程泥潭的人們提供了一些便利司抱,但它也讓我們對(duì)每種方式的工作原理以及它們?cè)趹?yīng)用時(shí)的細(xì)微差別的理解變得很難原朝。
本次 codelab 的目標(biāo)是通過(guò)回顧 callbacks 和promises 的用法燎竖、快速介紹 generators 址芯,然后對(duì) generators 和 async/await 異步編程的工作原理提供一個(gè)直觀(guān)的了解彰居,最終讓你能夠得心應(yīng)手的在不同的場(chǎng)景下應(yīng)用合適的編程方式诚纸。
本文假設(shè)您已經(jīng)之前的異步編程中使用過(guò)callbacks,promises和generators陈惰,并且對(duì)于Javascript閉包和柯里化(currying)相當(dāng)熟悉畦徘。
回調(diào)地獄
回調(diào)就是一切的開(kāi)始。由于事件循環(huán)抬闯,強(qiáng)烈建議不要在Javascript中執(zhí)行同步I / O或阻塞井辆,因此,為了執(zhí)行任何類(lèi)型的I / O或推遲執(zhí)行任何操作溶握,異步代碼運(yùn)行的策略是傳入一個(gè)函數(shù)以便稍后在事件循環(huán)中的某個(gè)地方觸發(fā)時(shí)進(jìn)行調(diào)用杯缺。一個(gè)回調(diào)并不算多壞,但是隨著代碼量的增長(zhǎng)睡榆,并且回調(diào)通常會(huì)導(dǎo)致越來(lái)越多的回調(diào)調(diào)用其他回調(diào)萍肆。最終看起來(lái)像這樣:
getUserData(function doStuff(e, a) {
getMoreUserData(function doMoreStuff(e, b) {
getEvenMoreUserData(function doEvenMoreStuff(e, c) {
getYetMoreUserData(function doYetMoreStuff(e, c) {
console.log('Welcome to callback hell!');
});
});
});
})
除了看著不斷嵌套的代碼起一身雞皮疙瘩袍榆,您還要把對(duì) do*Stuff 邏輯的控制權(quán)交給了其他函數(shù)(get*UserData()),而這些函數(shù)可能有也可能沒(méi)有源碼塘揣,您也無(wú)法真正分辨它們是否已經(jīng)調(diào)用過(guò)您的回調(diào)包雀。是不是很好?
Promises
Promises 取得了對(duì)回調(diào)提供的反轉(zhuǎn)控制亲铡,讓您將回調(diào)地獄轉(zhuǎn)變?yōu)橐粭l鏈?zhǔn)酱a才写。我們可以將最后一個(gè)示例轉(zhuǎn)換為下面的代碼:
getUserData()
.then(getUserData)
.then(doMoreStuff)
.then(getEvenMoreUserData)
.then(doEvenMoreStuff)
.then(getYetMoreUserData)
.then(doYetMoreStuff);
是不是看上去很簡(jiǎn)陋?可是等等=甭赞草!讓我們看一個(gè)更實(shí)際(但仍然很虛構(gòu))的回調(diào)示例:
// Suppose that we have a method fetchJson() that does GET requests and has an interface that looks
// like this, where callback is expected to take error as its first argument and the parsed response
// data as its second.
function fetchJson(url, callback) { ... }
fetchJson('/api/user/self', function(e, user) {
fetchJson('/api/interests?userId=' + user.id, function(e, interests) {
var recommendations = [];
interests.forEach(function () {
fetchJson('/api/recommendations?topic=' + interest, function(e, recommendation) {
recommendations.push(recommendation);
if (recommendations.length == interests.length) {
render(profile, interests, recommendations);
}
});
});
});
});
因此,我們先獲取到用戶(hù)的個(gè)人資料锭硼,然后獲取用戶(hù)的興趣房资,然后根據(jù)用戶(hù)的興趣獲取推薦內(nèi)容,最后在獲取到所有的推薦內(nèi)容時(shí)展現(xiàn)在頁(yè)面上檀头。盡管它一系列的callbacks讓人毛骨悚然轰异,您一定會(huì)為之感到自豪,但是promise會(huì)使這一切變得更好暑始,不是嗎搭独?
讓我們?cè)囋囉胮romise替代callback來(lái)改造fetchedJson(),promise處理了格式為JSON的響應(yīng)內(nèi)容到下一個(gè)執(zhí)行方法中廊镜。
fetchJson('/api/user/self')
.then(function (user) {
return fetchJson('/api/user/interests?userId=' + self.id);
})
.then(function (interests) {
return Promise.all[interests.map(i => fetchJson('/api/recommendations?topic=' + i))];
})
.then(function (recommendations) {
render(user, interests, recommendations);
});
是不是看上去美多了牙肝?但是讓我們看看代碼有沒(méi)有什么問(wèn)題?
糟糕嗤朴!我們?cè)阪溨凶詈笠粋€(gè)函數(shù)里無(wú)法獲取到個(gè)人資料和興趣配椭,因此這段代碼不起作用!那我們可以做什么呢雹姊?好吧股缸,我們可以再嵌套promise:
fetchJson('/api/user/self')
.then(function (user) {
return fetchJson('/api/user/interests?userId=' + self.id)
.then(interests => {
user: user,
interests: interests
});
})
.then(function (blob) {
return Promise.all[blob.interests.map(i => fetchJson('/api/recommendations?topic=' + i))]
.then(recommendations => {
user: blob.user,
interests: blob.interests,
recommendations: recommendations
});
})
.then(function (bigBlob) {
render(bigBlob.user, bigBlob.interests, bigBlob.recommendations);
});
好吧……現(xiàn)在這比我們期望的要丑陋得多,這種嵌套瘋狂不是我們想要擺脫回調(diào)地獄的原因之一嗎吱雏?現(xiàn)在怎么辦敦姻?
實(shí)際上,我們可以通過(guò)利用閉包來(lái)使代碼更優(yōu)美:
// We declare these variables we want to save ahead of time.
var user, recommendations;
fetchJson('/api/user/self')
.then(function (fetchedUser) {
user = fetchedUser;
return fetchJson('/api/user/interests?userId=' + self.id);
})
.then(function (fetchedInterests) {
interests = fetchedInterests;
return Promise.all(interests.map(i => fetchJson('/api/recommendations?topic=' + i)));
})
.then(function (recomendations) {
render(user, interests, recommendations);
})
.then(function () {
console.log('We are done!');
});
好吧歧杏,這幾乎和我們想要的一樣好镰惦,除了有點(diǎn)古怪。如果你注意到我們?cè)趐romise的callback中調(diào)用的參數(shù)是fetchedUser 和 fetchedInterests犬绒,而不是user 和 interests旺入,那證明你的觀(guān)察很仔細(xì)。
這種方法的缺陷在于凯力,您必須要注意茵瘾,您在內(nèi)部函數(shù)中命名的變量名稱(chēng)不能和即將在閉包中使用的“緩存”變量的名稱(chēng)相同急膀。如果您命名了一個(gè)相同名稱(chēng)的變量,那么在閉包中“緩存”變量就獲取不到龄捡。即使您盡量足夠小心以免產(chǎn)生影響卓嫂,但是在閉包中引用變量還是件很危險(xiǎn)和棘手的事。
異步Generators
Generators來(lái)拯救你了聘殖。如果使用generators晨雳,我們可以消除所有麻煩。相信我奸腺,這是真的很神奇餐禁,讓我們來(lái)看一看:
co(function* () {
var user = yield fetchJson('/api/user/self');
var interests = yield fetchJson('/api/user/interests?userId=' + self.id);
var recommendations = yield Promise.all(
interests.map(i => fetchJson('/api/recommendations?topic=' + i)));
render(user, interests, recommendations);
});
就是這樣,而且能夠運(yùn)行突照。您是否對(duì)generators的美麗感到不高興帮非,而且對(duì)在javascript擁有g(shù)enerators之前自己實(shí)際上是很魯莽地學(xué)習(xí)了Javascript而感到遺憾呢?我知道我曾經(jīng)做過(guò)讹蘑。
但是……這一切如何運(yùn)作的末盔?這真的是魔術(shù)嗎?當(dāng)然不是座慰。讓我們破滅這種幻想陨舱。
Generators
在我們的示例中,Generators看起來(lái)很容易使用版仔,但是實(shí)際上有很多事情要做游盲。為了深入研究異步generators,我們需要更好地了解generators的行為以及它如何能夠擁有同步外觀(guān)的異步操作蛮粮。
好吧益缎,一個(gè)generator會(huì)產(chǎn)生一系列值:
function* counts(start) {
yield start + 1;
yield start + 2;
yield start + 3;
return start + 4;
}
const counter = counts(0);
console.log(counter.next()); // {value: 1, done: false}
console.log(counter.next()); // {value: 2, done: false}
console.log(counter.next()); // {value: 3, done: false}
console.log(counter.next()); // {value: 4, done: true}
console.log(counter.next()); // {value: undefined, done: true}
這非常直接,但是讓我們來(lái)說(shuō)一下到底發(fā)生了什么:
- const counter = counts(0);-初始化generator并將其保存到 counter變量中然想。generator處于掛起狀態(tài)莺奔,并且generator內(nèi)部的代碼尚未執(zhí)行。
- console.log(counter.next());-計(jì)算出yield 1又沾,并將1作為value返回弊仪,done是false熙卡,因?yàn)檫€有更多的yield 要完成杖刷。
- console.log(counter.next()); -下一個(gè)是2。
- console.log(counter.next());-下一個(gè)是3〔蛋現(xiàn)在我們?cè)谧詈罅嘶迹遣皇蔷徒Y(jié)束了?不颓鲜,執(zhí)行在yield3處暫停表窘,我們需要再次調(diào)用next()以完成操作典予。
- console.log(counter.next()); -接下來(lái)是4,并且返回而不是yielded乐严,所以我們退出并完成操作瘤袖。
- console.log(counter.next());-生成器已經(jīng)完成!除了done之外昂验,沒(méi)有其他的可以返回捂敌。
我們現(xiàn)在了解generators如何工作的,但是請(qǐng)稍等既琴,還有個(gè)令人震驚的真相:generators不僅僅會(huì)吐出values占婉,也能夠吞掉values!
function* printer() {
console.log("We are starting!");
console.log(yield);
console.log(yield);
console.log(yield);
console.log("We are done!");
}
const counter = printer();
counter.next(1); // We are starting!
counter.next(2); // 2
counter.next(3); // 3
counter.next(4); // 4\n We are done!
counter.next(5); // <doesn't print anything>
哇甫恩,什么逆济?!generator正在消費(fèi)values而不是生成它們磺箕。這怎么可能奖慌?
但是這個(gè)秘密就在于next函數(shù),它不僅從generator返回values松靡,而且可以將values發(fā)送回generator升薯。當(dāng)next()給定一個(gè)參數(shù)時(shí),generator當(dāng)前正在等待的yield實(shí)際上被認(rèn)為是一個(gè)參數(shù)击困。這就是為什么第一次調(diào)用counter.next(1) 記錄是undefined涎劈,此時(shí)還沒(méi)有yield被resolve。
如果generators讓調(diào)用者代碼(routine)和generator代碼(routine)作為搭檔一起運(yùn)行阅茶,它們會(huì)在執(zhí)行并彼此等待時(shí)互相來(lái)回傳遞 values蛛枚。這就好像Javascript中的generators是為您設(shè)計(jì)的能夠?qū)崿F(xiàn)合作并發(fā)執(zhí)行的routines或“co-routines”。這難道不是看起來(lái)有點(diǎn)像 co()嗎脸哀?
但是蹦浦,讓我們不要停止學(xué)習(xí),超越自己撞蜂。這項(xiàng)示例的目的是建立關(guān)于generators和異步編程的感觀(guān)盲镶,還有什么比寫(xiě)一個(gè)generator更好的方式來(lái)建立對(duì)于generators的感受?不需要編寫(xiě)或使用任何generator函數(shù)蝌诡,而是要實(shí)現(xiàn)generator函數(shù)的內(nèi)部代碼溉贿。
Generator內(nèi)部 —生成generators
好吧,我實(shí)際上并不知道在各種JS運(yùn)行時(shí)中浦旱,generator的內(nèi)部是什么樣的宇色。但是這并不重要。Generators遵循一個(gè)接口,通過(guò)一個(gè)“constructor”實(shí)例化一個(gè)generator宣蠕,提供了next(value? : any) 方法來(lái)告訴generator繼續(xù)執(zhí)行并且提供values例隆,以及拋出異常的throw(error)方法,而不是我們會(huì)忽視的一個(gè)value和return()方法抢蚀。如果我們能夠滿(mǎn)足接口要求镀层,那很好。
因此皿曲,讓我們根據(jù)上面的描述嘗試實(shí)現(xiàn)一個(gè)counts() generator鹿响,然后使用不帶function*關(guān)鍵字的ES5來(lái)編寫(xiě)。因?yàn)樗鼪](méi)有采取任何輸入谷饿,我們可以忽略throw()并傳遞當(dāng)前value 到next()里惶我。那我們?cè)撛趺醋瞿兀?/p>
好吧,實(shí)際上還有另一種暫停和繼續(xù)執(zhí)行Javascript程序的方法:閉包博投!這看起來(lái)是不是很熟悉绸贡?
function makeCounter() {
var count = 1;
return function () {
return count++;
}
}
var counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
如果您以前使用過(guò)閉包,那么我可以肯定您在過(guò)去寫(xiě)過(guò)類(lèi)似的東西毅哗。函數(shù)makeCounter執(zhí)行后的返回值是一個(gè)函數(shù)听怕,就像一個(gè)generator一樣可以生成無(wú)限個(gè)數(shù)字。
但是虑绵,此函數(shù)不遵守generator接口尿瞭,也不直接適用于我們的counts()示例,返回了4個(gè)值并結(jié)束翅睛。我們?nèi)绾瓮ㄟ^(guò)一個(gè)通用方法來(lái)實(shí)現(xiàn)類(lèi)似generator的函數(shù)呢声搁?
閉包,狀態(tài)機(jī)以及繁重的工作捕发!
function counts(start) {
let state = 0;
let done = false;
function go() {
let result;
switch (state) {
case 0:
result = start + 1;
state = 1;
break;
case 1:
result = start + 2;
state = 2;
break;
case 2:
result = start + 3;
state = 3;
break;
case 3:
result = start + 4;
done = true;
state = -1;
break;
default:
break;
}
return {done: done, value: result};
}
return {
next: go
}
}
const counter = counts(0);
console.log(counter.next()); // {value: 1, done: false}
console.log(counter.next()); // {value: 2, done: false}
console.log(counter.next()); // {value: 3, done: false}
console.log(counter.next()); // {value: 4, done: true}
console.log(counter.next()); // {value: undefined, done: true}
如果運(yùn)行這段代碼疏旨,您將看到我們得到與generator版本相同的結(jié)果。整潔吧扎酷?
這是一個(gè)簡(jiǎn)單的狀態(tài)機(jī)檐涝,可以通過(guò)調(diào)用.next()可以轉(zhuǎn)換每個(gè)yield 語(yǔ)句的狀態(tài)。把它繪制成圖表法挨,看起來(lái)像這樣:
好的谁榜,既然我們已經(jīng)解構(gòu)了generator-as-producer,那么我們?nèi)绾稳?shí)現(xiàn)generator-as-consumer呢凡纳?
實(shí)際上沒(méi)有太大區(qū)別窃植。
function printer(start) {
let state = 0;
let done = false;
function go(input) {
let result;
switch (state) {
case 0:
console.log("We are starting!");
state = 1;
break;
case 1:
console.log(input);
state = 2;
break;
case 2:
console.log(input);
state = 3;
break;
case 3:
console.log(input);
console.log("We are done!");
done = true;
state = -1;
break;
default:
break;
return {done: done, value: result};
}
}
return {
next: go
}
}
const counter = printer();
counter.next(1); // We are starting!
counter.next(2); // 2
counter.next(3); // 3
counter.next(4); // 4
counter.next(5); // We are done!
我們要做的就是將輸入作為參數(shù)來(lái)運(yùn)行,并且通過(guò)管道傳遞值惫企。有點(diǎn)神奇吧撕瞧?幾乎像generators一樣神奇。
好極了狞尔!現(xiàn)在丛版,我們已經(jīng)實(shí)現(xiàn)了一個(gè)generator-as-producer和一個(gè)enerator-as-consumer。我們?yōu)槭裁床辉囋噷?shí)現(xiàn)一個(gè)generator-as-producer-and-consumer呢偏序?
這是另一個(gè)人造的 generator:
function* adder(initialValue) {
let sum = initialValue;
while (true) {
sum += yield sum;
}
}
由于我們現(xiàn)在是generator專(zhuān)家页畦,因此我們了解到此generator將next(value)中傳入的值加到sum上,并返回得到的sum研儒。它的行為就像我們期望的那樣:
const add = adder(0);
console.log(add.next()); // 0
console.log(add.next(1)); // 1
console.log(add.next(2)); // 3
console.log(add.next(3)); // 6
很酷≡ビВ現(xiàn)在,讓我們將這個(gè)接口用常規(guī)函數(shù)來(lái)實(shí)現(xiàn)端朵!
function adder(initialValue) {
let state = 'initial';
let done = false;
let sum = initialValue;
function go(input) {
let result;
switch (state) {
case 'initial':
result = initialValue;
state = 'loop';
break;
case 'loop':
sum += input;
result = sum;
state = 'loop';
break;
default:
break;
}
return {done: done, value: result};
}
return {
next: go
}
}
function runner() {
const add = adder(0);
console.log(add.next()); // 0
console.log(add.next(1)); // 1
console.log(add.next(2)); // 3
console.log(add.next(3)); // 6
}
runner();
我們已經(jīng)實(shí)現(xiàn)了一個(gè)真正的協(xié)例好芭。runner() 給adder()傳入了值,adder()相加并返回總和冲呢,然后 runner()打印sum值并給出一個(gè)新的value讓adder()來(lái)加舍败。
關(guān)于generators,我們還有更多內(nèi)容需要討論敬拓。異常如何工作邻薯?好吧,generator拋出的異常非常容易:該異常從next()冒泡至調(diào)用者乘凸,一直到generator結(jié)束厕诡。另一個(gè)方向,將異常從調(diào)用者至generator的傳遞营勤,取決于我們隱藏的throw() 方法灵嫌。
為了演示這一點(diǎn),讓我們?yōu)閍dder提供一個(gè)瘋狂的新功能葛作。如果調(diào)用者 .throw() 拋出一個(gè)異常到generator醒第,則generator會(huì)將sum作為返回的最后一個(gè)值。實(shí)現(xiàn)起來(lái)看起來(lái)就像這樣:
function* adder(initialValue) {
let sum = initialValue;
let lastSum = initialValue;
let temp;
while (true) {
try {
temp = sum;
sum += yield sum;
lastSum = temp;
} catch (e) {
sum = lastSum;
}
}
}
const add = adder(0);
console.log(add.next()); // 0
console.log(add.next(1)); // 1
console.log(add.next(2)); // 3
console.log(add.throw(new Error('BOO)!'))); // 1
console.log(add.next(4)); // 5
Generator內(nèi)部 — 錯(cuò)誤傳播
Oh boy进鸠,我們?nèi)绾味xthrow()稠曼?
簡(jiǎn)單!錯(cuò)誤只是另一個(gè)值客年。我們可以將其作為另一個(gè)參數(shù)傳遞給go()霞幅。請(qǐng)注意,我們實(shí)際上必須在這里稍加小心量瓜。當(dāng) throw(e)被調(diào)用時(shí)司恳,generator內(nèi)部的yield將如同其寫(xiě)成throw e一樣的效果。這意味著我們實(shí)際上應(yīng)該檢查狀態(tài)機(jī)中每個(gè)狀態(tài)的錯(cuò)誤绍傲,一旦無(wú)法處理扔傅,就會(huì)拋出異常耍共。
在以前的adder實(shí)現(xiàn)的基礎(chǔ)上,我們必須添加一些錯(cuò)誤檢查和try/catch塊猎塞。
function adder(initialValue) {
let state = 'initial';
let done = false;
let sum = initialValue;
let lastSum;
let temp;
function go(input, err) {
let result;
switch (state) {
case 'initial':
if (err) {
throw err;
}
temp = sum;
result = initialValue;
state = 'loop';
break;
case 'loop':
try {
if (err) {
throw err;
}
sum += input;
lastSum = temp;
temp = sum;
} catch (e) {
sum = lastSum;
}
result = sum;
state = 'loop';
break;
default:
break;
}
return {done: done, value: result};
}
return {
next: go,
throw: function (err) {
return go(undefined, err)
}
}
}
function runner() {
const add = adder(0);
console.log(add.next()); // 0
console.log(add.next(1)); // 1
console.log(add.next(2)); // 3
console.log(add.throw(new Error('BOO)!'))); // 1
console.log(add.next(4)); // 5
}
runner();
完美试读!我們已經(jīng)實(shí)現(xiàn)了一組協(xié)同例程,它們可以像真正的生成器一樣互相傳遞消息和異常荠耽。
但這實(shí)際上變得相當(dāng)復(fù)雜钩骇,這只是說(shuō)明了generators在幕后為我們做了多少事。我們使用generators免費(fèi)提供給我們的閉包就可以手動(dòng)做很多事情铝量。
- 當(dāng)閉包的狀態(tài)可以?huà)炱饡r(shí)倘屹,錯(cuò)誤傳播和處理的執(zhí)行很復(fù)雜
- 實(shí)際上,為了正確地暫停循環(huán)慢叨,Loops必須以笨拙的方式“unrolled”纽匙。我們的loop示例實(shí)際上是運(yùn)行while循環(huán)的后半部分,而運(yùn)行前半部分拍谐,以匹配generator中的yield哄辣。
我們不必自己實(shí)現(xiàn)generators是一件好事情。
你做到了T病力穗!我們已經(jīng)深入探討了generators的潛在實(shí)現(xiàn)方式,并希望您對(duì)發(fā)generators的工作方式有了更直觀(guān)的了解气嫁。綜上所述:
- generators可以產(chǎn)生或消費(fèi)值当窗,或兩者都有
- generators的狀態(tài)可以暫停(狀態(tài),狀態(tài)機(jī)寸宵,明白崖面?)
- 一個(gè)調(diào)用者和一個(gè)generators可以形成一組相互協(xié)作的協(xié)同例程
- 異常可以向任一方向發(fā)送梯影。
現(xiàn)在巫员,我們有了一個(gè)更好的理解,一種對(duì)generators潛在有用的思考是我們編寫(xiě)并發(fā)運(yùn)行的例程的語(yǔ)法甲棍,而該例程可以通過(guò)單個(gè)值通道(yield語(yǔ)句)來(lái)相互傳遞消息 简识。在下一部分中,這將很有用感猛,我們的co()將從協(xié)例程中派生實(shí)現(xiàn)七扰。
使用協(xié)程進(jìn)行控制反轉(zhuǎn)
現(xiàn)在我們是generator專(zhuān)家,讓我們考慮如何將generators應(yīng)用于異步編程中來(lái)陪白。能夠自己編寫(xiě)generators并不意味著generators內(nèi)的Promise會(huì)自動(dòng)得到resolved颈走。但是,等等咱士,generators并不是設(shè)計(jì)成自己工作立由。它們被設(shè)計(jì)為與另一個(gè)調(diào)用.next()和.throw()的程序(主程序)一起工作轧钓。
如果不是將業(yè)務(wù)邏輯放在主要例程中,而是將所有業(yè)務(wù)邏輯放在generator中锐膜,該怎么辦毕箍?每次業(yè)務(wù)邏輯遇到諸如Promise之類(lèi)的異步值時(shí),generator都會(huì)發(fā)出這樣的消息:“我不想處理這種在解決時(shí)就喚醒我的瘋狂”枣耀,然后暫停并將Promise扔給服務(wù)例程霉晕,同時(shí)服務(wù)例程決定:“很好庭再,我稍后再調(diào)用您”飒箭。然后木蹬,服務(wù)例程會(huì)在Promise上注冊(cè)一個(gè)callback,然后退出,當(dāng)Promise解析時(shí)等待事件循環(huán)調(diào)用它车胡。完成后,它會(huì)“嘿蝗茁,準(zhǔn)備好了娜汁,輪到您了”,然后通過(guò).next()發(fā)送值進(jìn)入休眠的generator斧抱,等待generator執(zhí)行其操作常拓,然后返回另一個(gè)異步的事項(xiàng)來(lái)處理……等等之類(lèi)的工作。所以關(guān)于服務(wù)例程如何為generator始終提供服務(wù)的是一個(gè)令人悲傷的故事辉浦。
現(xiàn)在讓我們回到主要主題弄抬。鑒于我們對(duì)generators和promises如何工作的了解,對(duì)于我們來(lái)說(shuō)宪郊,創(chuàng)建這個(gè)“服務(wù)例程”應(yīng)該并不困難掂恕。服務(wù)例程本身將作為Promise同時(shí)執(zhí)行,實(shí)例化并服務(wù)于generator弛槐,然后通過(guò).then()回調(diào)將最終結(jié)果返回給我們的主例程懊亡。
現(xiàn)在讓我們回頭看看co()程序。co()是從動(dòng)的服務(wù)例程乎串,以便generator只能使用同步值來(lái)工作〉暝妫現(xiàn)在是不是變得更有意義了?
var user = yield fetchJson('/api/user/self');
var interests = yield fetchJson('/api/user/interests?userId=' + self.id);
var recommendations = yield Promise.all(
interests.map(i => fetchJson('/api/recommendations?topic=' + i)));
render(user, interests, recommendations);
});
對(duì)于那些熟悉蹦床函數(shù)的人叹誉,co()
可以將其視為一個(gè)蹦床函數(shù)的異步版本艰争,即蹦床Promises。
co()的簡(jiǎn)化版本
很好桂对!現(xiàn)在讓我們自己來(lái)實(shí)現(xiàn)co()甩卓,以建立一些關(guān)于此從屬例程工作原理的感觀(guān)。co()必須
- 返回一個(gè)以供調(diào)用者來(lái)等待
- 實(shí)例化generator
- 調(diào)用generator上的.next()以獲取第一個(gè) yielded 結(jié)果蕉斜,該結(jié)果的形式應(yīng)為 {done: false, value: [a Promise]}
- 在Promise上注冊(cè)一個(gè)callback
- 當(dāng)promise解析(callback被調(diào)用)后逾柿,使用resolved的值調(diào)用generator的.next()缀棍,并獲取另一個(gè)值返回
- 從步驟4重復(fù)
- 如果generator在任何時(shí)候返回 {done: true, value: ...},resolve由co()返回的promise
現(xiàn)在机错,讓我們不用擔(dān)心錯(cuò)誤爬范,來(lái)實(shí)現(xiàn)一個(gè)可以處理以下人工示例的co()方法:
function deferred(val) {
return new Promise((resolve, reject) => resolve(val));
}
co(function* asyncAdds() {
console.log(yield deferred(1)); // 1
console.log(yield deferred(2)); // 2
console.log(yield deferred(3)); // 3
return 4;
}).then(function (result) {
console.log(result); // 4
});
function co(generator) {
return new Promise((resolve, reject) => {
// Your code goes here
});
}
如果您有一臺(tái)可用的計(jì)算機(jī),我建議在繼續(xù)閱讀下面的解決方案之前弱匪,請(qǐng)先嘗試下自己實(shí)現(xiàn)該解決方案青瀑。相比只是閱讀解決方法而不去嘗試實(shí)現(xiàn),這能夠更好地幫助您更好地理解co()萧诫。
該實(shí)現(xiàn)應(yīng)最終看起來(lái)像這樣:
function co(generator) {
return new Promise((resolve, reject) => {
const g = generator();
function next(nextVal) {
const ret = g.next(nextVal);
if (ret.done) {
return resolve(ret.value);
}
ret.value.then(next);
}
next();
});
}
還不錯(cuò)吧斥难?用大約10行代碼,我們實(shí)現(xiàn)了曾經(jīng)神奇和全能的核心功能co()帘饶。讓我們看看是否可以再增加點(diǎn)什么哑诊。異常處理如何?
擁有異常處理的co()
當(dāng)generator產(chǎn)生的Promise被拒絕時(shí)及刻,我們想要co()可以將異常通知給generator例程镀裤。請(qǐng)記住,generator接口為我們提供了一個(gè)發(fā)送異常的方法.throw()缴饭。
從此擁有全新的deferReject 函數(shù)的模板開(kāi)始暑劝,讓我們嘗試實(shí)現(xiàn)一個(gè)可以處理被拒絕的諾言的co()方法。
function deferred(val) {
return new Promise((resolve, reject) => resolve(val));
}
function deferReject(e) {
return new Promise((resolve, reject) => reject(e));
}
co(function* asyncAdds() {
console.log(yield deferred('We are starting!'));
try {
console.log(yield deferReject(new Error('To fail, or to not fail.')));
} catch (e) {
console.log('We recovered!');
}
console.log(yield deferred('We finished!'));
});
function co(generator) {
return new Promise((resolve, reject) => {
// Your code goes here
});
}
異常處理有些棘手颗搂。取決于產(chǎn)生的promise是否已resolved或rejected担猛,我們需要不同的callbacks,因此我們希望將next()函數(shù)的代碼拆分為單獨(dú)的onResolve()和 onReject()方法峭火。onResolve()將負(fù)責(zé)調(diào)用generator的.next()毁习,而onResolve()將負(fù)責(zé)調(diào)用.throw()。 如果generator拋出錯(cuò)誤(而不是產(chǎn)生rejected promise)卖丸,則兩個(gè)callbacks都將用try/catch包裝自身以便立即拒絕co()的promise纺且。
同樣,如果您有一臺(tái)可用的計(jì)算機(jī)稍浆,建議您先嘗試一下自己解決载碌,然后再繼續(xù)向下查看解決方案。
添加新邏輯并不像聽(tīng)起來(lái)那樣復(fù)雜衅枫,最終會(huì)變成如下所示:
function co(generator) {
return new Promise((resolve, reject) => {
const g = generator();
function onResolve(value) {
let ret;
try {
ret = g.next(value);
} catch (e) {
reject(e)
return;
}
next(ret);
}
function onReject(err) {
let ret;
try {
ret = g.throw(err);
} catch (e) {
reject(e);
return;
}
next(ret);
}
function next(ret) {
if (ret.done) {
return resolve(ret.value);
}
ret.value.then(onResolve, onReject);
}
onResolve();
});
}
到此嫁艇,我們已經(jīng)實(shí)現(xiàn)co()
!幾乎弦撩!co()
還支持thunks步咪,嵌套generators,上述數(shù)組和上述深層對(duì)象益楼。但是猾漫,現(xiàn)在是不是看上去不再那么神奇了点晴?
作為co()
自己完成任務(wù)的一種額外方法,讓我們進(jìn)行實(shí)際操作(按照此鏈接)來(lái)真正完成co()
悯周。是不是看起來(lái)幾乎一樣酷粒督?
圣杯:async/await
是的,現(xiàn)在我們理解了generators和co()禽翼。但這對(duì)我們使用 async/await有用嗎屠橄?答案是肯定的!到目前為止闰挡,我們已經(jīng)建立的理解讓我們很容易理解async/await锐墙。
async關(guān)鍵字允許我們可以聲明一個(gè)可以被await關(guān)鍵字暫停的函數(shù),就像generators可以用yield 關(guān)鍵字來(lái)暫停解总。await只能在Promises上使用贮匕,并且只能在被async包裹的函數(shù)的執(zhí)行堆棧中使用姐仅。async函數(shù)在執(zhí)行時(shí)返回Promises花枫。
所以要像下面使用async/await替代generators來(lái)轉(zhuǎn)換函數(shù),你基本上要用async替換co()和用await替換yield掏膏,并從函數(shù)上刪除*劳翰,因此它不再是一臺(tái)generator。
co(function* () {
var user = yield fetchJson('/api/user/self');
var interests = yield fetchJson('/api/user/interests?userId=' + self.id);
var recommendations = yield Promise.all(
interests.map(i => fetchJson('/api/recommendations?topic=' + i)));
render(user, interests, recommendations);
});
變成:
async function () {
var user = await fetchJson('/api/user/self');
var interests = await fetchJson('/api/user/interests?userId=' + self.id);
var recommendations = await Promise.all(
interests.map(i => fetchJson('/api/recommendations?topic=' + i)));
render(user, interests, recommendations);
}();
但是有一些小怪異/差異要注意:
- co()立即執(zhí)行異步generator馒疹。async創(chuàng)建了函數(shù)佳簸,但是您仍然必須調(diào)用它。async更像是co()變體 co.wrap()颖变。
- 使用co()生均,您可以yield Promises,thunks腥刹,Promises 數(shù)組或Promises 對(duì)象马胧。使用async,您只能在Promises上使用await衔峰。
- 您不能使用async到generators的await佩脊。那是沒(méi)有道理的。但是您可以co()用來(lái)將用異步generators編寫(xiě)的代碼包裝到Promise中垫卤,然后再使用await 威彰。
結(jié)語(yǔ)
我們回顧了Javascript異步編程的全部簡(jiǎn)短歷史,找出了generators和co()“幕后”的工作方式穴肘,然后了解了如何將學(xué)習(xí)到的內(nèi)容應(yīng)用于async/await歇盼。你應(yīng)該感到自豪。