您好,登錄后才能下訂單哦!
異步編程一直是JavaScript 編程的重大事項(xiàng)。關(guān)于異步方案, ES6 先是出現(xiàn)了 基于狀態(tài)管理的 Promise,然后出現(xiàn)了 Generator 函數(shù) + co 函數(shù),緊接著又出現(xiàn)了 ES7 的 async + await 方案。
本文力求以最簡(jiǎn)明的方式來(lái)疏通 async + await。
異步編程的幾個(gè)場(chǎng)景
先從一個(gè)常見問題開始:一個(gè)for 循環(huán)中,如何異步的打印迭代順序?
我們很容易想到用閉包,或者 ES6 規(guī)定的 let 塊級(jí)作用域來(lái)回答這個(gè)問題。
for (let val of [1, 2, 3, 4]) { setTimeout(() => console.log(val),100); } // => 預(yù)期結(jié)果依次為:1, 2, 3, 4
這里描述的是一個(gè)均勻發(fā)生的的異步,它們被依次按既定的順序排在異步隊(duì)列中等待執(zhí)行。
如果異步不是均勻發(fā)生的,那么它們被注冊(cè)在異步隊(duì)列中的順序就是亂序的。
for (let val of [1, 2, 3, 4]) { setTimeout(() => console.log(val), 100 * Math.random()); } // => 實(shí)際結(jié)果是隨機(jī)的,依次為:4, 2, 3, 1
返回的結(jié)果是亂序不可控的,這本來(lái)就是最為真實(shí)的異步。但另一種情況是,在循環(huán)中,如果希望前一個(gè)異步執(zhí)行完畢、后一個(gè)異步再執(zhí)行,該怎么辦?
for (let val of ['a', 'b', 'c', 'd']) { // a 執(zhí)行完后,進(jìn)入下一個(gè)循環(huán) // 執(zhí)行 b,依此類推 }
這不就是多個(gè)異步 “串行” 嗎!
在回調(diào) callback 嵌套異步操作、再回調(diào)的方式,不就解決了這個(gè)問題!或者,使用 Promise + then() 層層嵌套同樣也能解決問題。但是,如果硬是要將這種嵌套的方式寫在循環(huán)中,還恐怕還需費(fèi)一番周折。試問,有更好的辦法嗎?
異步同步化方案
試想,如果要去將一批數(shù)據(jù)發(fā)送到服務(wù)器,只有前一批發(fā)送成功(即服務(wù)器返回成功的響應(yīng)),才開始下一批數(shù)據(jù)的發(fā)送,否則終止發(fā)送。這就是一個(gè)典型的 “for 循環(huán)中存在相互依賴的異步操作” 的例子。
明顯,這種 “串行” 的異步,實(shí)質(zhì)上可以當(dāng)成同步。它和亂序的異步比較起來(lái),花費(fèi)了更多的時(shí)間。按理說,我們希望程序異步執(zhí)行,就是為了 “跳過” 阻塞,較少時(shí)間花銷。但與之相反的是,如果需要一系列的異步 “串行”,我們應(yīng)該怎樣很好的進(jìn)行編程?
對(duì)于這個(gè) “串行” 異步,有了 ES6 就非常容易的解決了這個(gè)問題。
async function task () { for (let val of [1, 2, 3, 4]) { // await 是要等待響應(yīng)的 let result = await send(val); if (!result) { break; } } } task();
從字面上看,就是本次循環(huán),等有了結(jié)果,再進(jìn)行下一次循環(huán)。因此,循環(huán)每執(zhí)行一次就會(huì)被暫停(“卡住”)一次,直到循環(huán)結(jié)束。這種編碼實(shí)現(xiàn),很好的消除了層層嵌套的 “回調(diào)地獄” 問題,降低了認(rèn)知難度。
這就是異步問題同步化的方案。關(guān)于這個(gè)方案,如果說 Promise 主要解決的是異步回調(diào)問題,那么 async + await 主要解決的就是將異步問題同步化,降低異步編程的認(rèn)知負(fù)擔(dān)。
async + await “外異內(nèi)同”
早先接觸這套 API 時(shí),看著繁瑣的文檔,一知半解的認(rèn)為 async + await 主要用來(lái)解決異步問題同步化的。
其實(shí)不然。從上面的例子看到:async 關(guān)鍵字聲明了一個(gè) 異步函數(shù),這個(gè) 異步函數(shù) 體內(nèi)有一行 await 語(yǔ)句,它告示了該行為同步執(zhí)行,并且與上下相鄰的代碼是依次逐行執(zhí)行的。
將這個(gè)形式化的東西再翻譯一下,就是:
1、async 函數(shù)執(zhí)行后,總是返回了一個(gè) promise 對(duì)象
2、await 所在的那一行語(yǔ)句是同步的
其中,1 說明了從外部看,task 方法執(zhí)行后返回一個(gè) Promise 對(duì)象,正因?yàn)樗祷氐氖?Promise,所以可以理解task 是一個(gè)異步方法。毫無(wú)疑問它是這樣用的:
task().then((val) => {alert(val)}) .then((val) => {alert(val)})
2 說明了在 task 函數(shù)內(nèi)部,異步已經(jīng)被 “削” 成了同步。整個(gè)就是一個(gè)執(zhí)行稍微耗時(shí)的函數(shù)而已。
綜合 1、2,從形式上看,就是 “task 整體是一個(gè)異步函數(shù),內(nèi)部整個(gè)是同步的”,簡(jiǎn)稱“外異內(nèi)同”。
整體是一個(gè)異步函數(shù) 不難理解。在實(shí)現(xiàn)上,我們不妨逆向一下,語(yǔ)言層面讓async關(guān)鍵字調(diào)用時(shí),在函數(shù)執(zhí)行的末尾強(qiáng)制增加一個(gè)promise 反回:
async fn () { let result; // ... //末尾返回 promise return isPromise(result)? result : Promise.resolve(undefined); }
內(nèi)部是同步的 是怎么做到的?實(shí)際上 await 調(diào)用,是讓后邊的語(yǔ)句(函數(shù))做了一個(gè)遞歸執(zhí)行,直到獲取到結(jié)果并使其 狀態(tài) 變更,才會(huì) resolve 掉,而只有 resolve 掉,await 那一行代碼才算執(zhí)行完,才繼續(xù)往下一行執(zhí)行。所以,盡管外部是一個(gè)大大的 for 循環(huán),但是整個(gè) for 循環(huán)是依次串行的。
因此,僅從上述框架的外觀出發(fā),就不難理解 async + await 的意義。使用起來(lái)也就這么簡(jiǎn)單,反而 Promise 是一個(gè)必須掌握的基礎(chǔ)件。
秉承本次《重讀 ES6》系列的原則,不過多追求理解細(xì)節(jié)和具體實(shí)現(xiàn)過程。我們繼續(xù)鞏固一下這個(gè) “形式化” 的理解。
async + await 的進(jìn)一步理解
有這樣的一個(gè)異步操作 longTimeTask,已經(jīng)用 Promise 進(jìn)行了包裝。借助該函數(shù)進(jìn)行一系列驗(yàn)證。
const longTimeTask = function (time) { return new Promise((resolve, reject) => { setTimeout(()=>{ console.log(`等了 ${time||'xx'} 年,終于回信了`); resolve({'msg': 'task done'}); }, time||1000) }) }
async 函數(shù)的執(zhí)行情況
如果,想查看 async exec1 函數(shù)的返回結(jié)果,以及 await 命令的執(zhí)行結(jié)果:
const exec1 = async function () { let result = await longTimeTask(); console.log('result after long time ===>', result); } // 查看函數(shù)內(nèi)部執(zhí)行順序 exec1(); // => 等了 xx 年,終于回信了 // => result after long time ===> Object {msg: "task done"} //查看函數(shù)總體返回值 console.log(exec1()); // => Promise {[[PromiseStatus]]: "pending",...} // => 同上
以上 2 步執(zhí)行,清晰的證明了 exec1 函數(shù)體內(nèi)是同步、逐行逐行執(zhí)行的,即先執(zhí)行完異步操作,然后進(jìn)行 console.log() 打印。而 exec1() 的執(zhí)行結(jié)果就直接是一個(gè) Promise,因?yàn)樗钕葧?huì)蹦出來(lái)一串 Promise ...,然后才是 exec1 函數(shù)的內(nèi)部執(zhí)行日志。
因此,所有驗(yàn)證,完全符合 整體是一個(gè)異步函數(shù),內(nèi)部整個(gè)是同步的 的總結(jié)。
await 如何執(zhí)行其后語(yǔ)句?
回到 await ,看看它是如何執(zhí)行其后邊的語(yǔ)句的。假設(shè):讓 longTimeTask() 后邊直接帶 then() 回調(diào),分兩種情況:
1)then() 中不再返回任何東西
2) then() 中繼續(xù)手動(dòng)返回另一個(gè) promise
const exec2 = async function () { let result = await longTimeTask().then((res) => { console.log('then ===>', res.msg); res.msg = `${res.msg} then refrash message`; // 注釋掉這條 return 或 手動(dòng)返回一個(gè) promise return Promise.resolve(res); }); console.log('result after await ===>', result.msg); } exec2(); // => 情況一 TypeError: Cannot read property 'msg' of undefined // => 情況二 正常
首先,longTimeTask() 加上再多得 then() 回調(diào),也不過是放在了它的回調(diào)列隊(duì) queue 里了。也就是說,await 命令之后始終是一條 表達(dá)式語(yǔ)句,只不過上述代碼書寫方式比較讓人迷惑。(比較好的實(shí)踐建議是,將 longTimeTask 方法身后的 then() 移入 longTimeTask 函數(shù)體封裝起來(lái))
其次,手動(dòng)返回另一個(gè) promise 和什么也不返回,關(guān)系到 longTimeTask() 方法最終 resolve 出去的內(nèi)容不一樣。換句話說,await 命令會(huì)提取其后邊的promise 的 resolve 結(jié)果,進(jìn)而直接導(dǎo)致 result 的不同。
值得強(qiáng)調(diào)的是,await 命令只認(rèn) resolve 結(jié)果,對(duì) reject 結(jié)果報(bào)錯(cuò)。不妨用以下的 return 語(yǔ)句替換上述 return 進(jìn)行驗(yàn)證。
return Promise.reject(res);
最后
其實(shí),關(guān)于異步編程還有很多可以梳理的,比如跨模塊的異步編程、異步的單元測(cè)試、異步的錯(cuò)誤處理以及什么是好的實(shí)踐。All in all, 限于篇幅,不在此匯總了。最后,async + await 確實(shí)是一個(gè)很優(yōu)雅的方案。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。