異步編程的終極解決方案 async/await:用同步的方式去寫(xiě)異步代碼
點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號(hào)
回復(fù)算法,加入前端編程面試算法每日一題群
早期的回調(diào)函數(shù)
回調(diào)函數(shù)我們經(jīng)常有寫(xiě)到,比如:
ajax(url, (res) => {
console.log(res);
})
復(fù)制代碼
但是這種回調(diào)函數(shù)有一個(gè)大缺陷,就是會(huì)寫(xiě)出 回調(diào)地獄(Callback hell)。
比如,如果多個(gè)回調(diào)存在依賴,可能會(huì)寫(xiě)成:
ajax(url, (res) => {
console.log(res);
// ...處理代碼
ajax(url2, (res2) => {
console.log(res2);
// ...處理代碼
ajax(url3, (res3) => {
console.log(res3);
// ...處理代碼
})
})
})
復(fù)制代碼
這個(gè)就是 回調(diào)地獄:
內(nèi)嵌函數(shù)存在耦合,牽一發(fā)而動(dòng)全身,改一個(gè)會(huì)影響其它地方 內(nèi)嵌函數(shù)多了,發(fā)生錯(cuò)誤要怎么處理呢?這是一個(gè)難題
早期回調(diào)函數(shù)的優(yōu)缺點(diǎn):
優(yōu)點(diǎn):解決了 同步阻塞 的問(wèn)題(只要有一個(gè)任務(wù)耗時(shí)很長(zhǎng),后面的任務(wù)都必須排隊(duì)等著,會(huì)拖延整個(gè)程序的執(zhí)行) 缺點(diǎn):回調(diào)地獄;不能用 try catch捕獲錯(cuò)誤;不能return
過(guò)渡方案 Generator
ES6 新引入了 Generator 函數(shù)(生成器函數(shù)),可以通過(guò) yield 關(guān)鍵字,把函數(shù)的執(zhí)行流掛起,為改變執(zhí)行流程提供了可能,從而為異步編程提供解決方案。最大的特點(diǎn)就是 可以控制函數(shù)的執(zhí)行。
Generator 有兩個(gè)區(qū)分于普通函數(shù)的部分:
一是在 function后面,函數(shù)名之前有個(gè)*,用來(lái)表示函數(shù)為Generator函數(shù)函數(shù)內(nèi)部有 yield表達(dá)式,用來(lái)定義函數(shù)內(nèi)部的狀態(tài)
Generator 函數(shù)的具體使用方式是:
在 Generator函數(shù)內(nèi)部執(zhí)行一段代碼,如果遇到yield關(guān)鍵字,那么 JS 引擎將返回關(guān)鍵字后面的內(nèi)容給外部,并暫停該函數(shù)的執(zhí)行。外部函數(shù)可以通過(guò) next方法恢復(fù)函數(shù)的執(zhí)行。
function* fn() {
console.log("one");
yield '1';
console.log("two");
yield '2';
console.log("three");
return '3';
}
復(fù)制代碼
調(diào)用 Generator 函數(shù)和調(diào)用普通函數(shù)一樣,在函數(shù)名后面加上 () 即可,但是 Generator 函數(shù)不會(huì)像普通函數(shù)一樣立即執(zhí)行,而是 返回一個(gè)指向內(nèi)部狀態(tài)對(duì)象的指針,所以要調(diào)用遍歷器對(duì)象 Iterator 的 next 方法,指針就會(huì)從函數(shù)頭部或者上一次停下來(lái)的地方開(kāi)始執(zhí)行。
如下:

next 方法:
一般情況下, next 方法不傳入?yún)?shù)的時(shí)候,yield 表達(dá)式的返回值是 undefined。當(dāng) next 傳入?yún)?shù)的時(shí)候,該參數(shù)會(huì)作為上一步 yield 的返回值。
Generator 生成器也是通過(guò)同步的方式寫(xiě)異步代碼的,也可以解決回調(diào)地獄的問(wèn)題,但是比較難以理解,希望下面的例子能夠幫助你理解 Generator 生成器:
function* sum(a) {
console.log('a:', a);
let b = yield 1;
console.log('b:', b);
let c = yield 2;
console.log('c:', c);
let sum = a + b + c;
console.log('sum:', sum)
return sum;
}
復(fù)制代碼
next不傳參時(shí),yield返回undefined
如下圖:

當(dāng)?shù)谝淮螆?zhí)行
next時(shí),傳參會(huì)被忽略,并且函數(shù)暫停在yield 1處,所以返回1當(dāng)?shù)诙螆?zhí)行
next時(shí),不傳參,那么yield 1返回的是undefined,所以b的值是undefined第三次同理,
c的值為undefined當(dāng)
next傳入?yún)?shù)時(shí),該參數(shù)會(huì)作為上一步yield的返回值
如下圖:

當(dāng)?shù)谝淮螆?zhí)行 next時(shí),傳參(20)會(huì)被忽略,并且函數(shù)暫停在yield 1處,所以返回1當(dāng)?shù)诙螆?zhí)行 next時(shí),傳參30,作為yield 1返回的值,所以b = yield 1,b的值是30當(dāng)?shù)诙螆?zhí)行 next時(shí),傳參40,作為yield 2返回的值,所以c = yield 2,c的值是40
協(xié)程
我們知道,async/await 是一個(gè)自動(dòng)執(zhí)行的 Generator 函數(shù),上面已經(jīng)介紹了 Generator 函數(shù),那么接下來(lái)很有必要介紹一下 V8 引擎是如何實(shí)現(xiàn)一個(gè)函數(shù)的暫停和恢復(fù) 的呢?
要搞懂函數(shù)為何能暫停和恢復(fù),首先要了解 協(xié)程 的概念。進(jìn)程和線程我們都知道,那么協(xié)程是什么呢?
協(xié)程是一種比線程更加輕量級(jí)的存在??梢园褏f(xié)程看成是跑在線程上的任務(wù),一個(gè)線程上可以存在多個(gè)協(xié)程,但是在線程上同時(shí)只能執(zhí)行一個(gè)協(xié)程,比如當(dāng)前執(zhí)行的是 A 協(xié)程,要啟動(dòng) B 協(xié)程,那么 A 協(xié)程就需要將主線程的控制權(quán)交給 B 協(xié)程,這就體現(xiàn)在 A 協(xié)程暫停執(zhí)行,B 協(xié)程恢復(fù)執(zhí)行;同樣,也可以從 B 協(xié)程中啟動(dòng) A 協(xié)程。通常,如果從 A 協(xié)程啟動(dòng) B 協(xié)程,我們就把 A 協(xié)程稱為 B 協(xié)程的父協(xié)程。
正如一個(gè)進(jìn)程可以擁有多個(gè)線程一樣,一個(gè)線程也可以擁有多個(gè)協(xié)程。最重要的是,協(xié)程不是被操作系統(tǒng)內(nèi)核所管理,而是完全由程序所控制(即在用戶態(tài)執(zhí)行)。這樣帶來(lái)的好處就是性能得到了很大的提升,不會(huì)像線程切換那樣消耗資源。
可以結(jié)合代碼理解:
function* genDemo() {
console.log("開(kāi)始執(zhí)行第一段")
yield 'generator 2'
console.log("開(kāi)始執(zhí)行第二段")
yield 'generator 2'
console.log("開(kāi)始執(zhí)行第三段")
yield 'generator 2'
console.log("執(zhí)行結(jié)束")
return 'generator 2'
}
console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')
復(fù)制代碼
執(zhí)行過(guò)程如下圖所示,可以重點(diǎn)關(guān)注協(xié)程之間的切換:
從圖中可以看出來(lái)協(xié)程的四點(diǎn)規(guī)則:
通過(guò)調(diào)用生成器函數(shù) genDemo來(lái)創(chuàng)建一個(gè) 協(xié)程gen,創(chuàng)建之后,gen協(xié)程并沒(méi)有立即執(zhí)行。要讓 gen協(xié)程執(zhí)行,需要通過(guò)調(diào)用gen.next。當(dāng)協(xié)程正在執(zhí)行的時(shí)候,可以 通過(guò) yield關(guān)鍵字來(lái)暫停gen協(xié)程的執(zhí)行,并返回主要信息給父協(xié)程。如果協(xié)程在執(zhí)行期間,遇到了 return關(guān)鍵字,那么 JS 引擎會(huì)結(jié)束當(dāng)前協(xié)程,并將return后面的內(nèi)容返回給父協(xié)程。
協(xié)程之間的切換:
gen協(xié)程和父協(xié)程是在主線程上交互執(zhí)行的,并不是并發(fā)執(zhí)行的,它們之前的切換是 通過(guò)yield和gen.next來(lái)配合完成 的。當(dāng)在 gen協(xié)程中調(diào)用了yield方法時(shí),JS 引擎會(huì)保存gen協(xié)程當(dāng)前的調(diào)用棧信息,并恢復(fù)父協(xié)程的調(diào)用棧信息。同樣,當(dāng)在父協(xié)程中執(zhí)行gen.next時(shí),JS 引擎會(huì)保存父協(xié)程的調(diào)用棧信息,并恢復(fù)gen協(xié)程的調(diào)用棧信息。

其實(shí)在 JS 中,Generator 生成器就是協(xié)程的一種實(shí)現(xiàn)方式。
成熟方案 Promise
關(guān)于 Promise,可以去看我上一篇文章:《異步編程 Promise:從使用到手寫(xiě)實(shí)現(xiàn)(4200字長(zhǎng)文)》,在這一篇文章中詳細(xì)介紹了 Promise 如何解決回調(diào)地獄的問(wèn)題,了解 Promise 和微任務(wù)的淵源,然后帶你一步一步的解構(gòu)手寫(xiě)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 Promise,最后簡(jiǎn)單介紹并手寫(xiě)實(shí)現(xiàn)了一些 Promise 的 API,包括 Promise.all、Promise.allSettled、Promise.race、Promise.finally 等API。
終極解決方案 async/await
使用 Promise 能很好地解決回調(diào)地獄的問(wèn)題,但是這種方式充滿了 Promise 的 then() 方法,如果處理流程比較復(fù)雜的話,那么整段代碼將充斥著 then,語(yǔ)義化不明顯,代碼不能很好地表示執(zhí)行流程。
基于這個(gè)原因,ES7 引入了 async/await,這是 JavaScript 異步編程的一個(gè)重大改進(jìn),提供了 在不阻塞主線程的情況下使用同步代碼實(shí)現(xiàn)異步訪問(wèn)資源的能力,并且使得代碼邏輯更加清晰。
其實(shí) async/await 技術(shù)背后的秘密就是 Promise 和 Generator 生成器應(yīng)用,往低層說(shuō)就是 微任務(wù)和協(xié)程應(yīng)用。要搞清楚 async 和 await 的工作原理,我們得對(duì) async 和 await 分開(kāi)分析。
async
async 到底是什么?根據(jù) MDN 定義,async 是一個(gè)通過(guò) 異步執(zhí)行并隱式返回 Promise 作為結(jié)果的函數(shù)。重點(diǎn)關(guān)注兩個(gè)詞:異步執(zhí)行和隱式返回 Promise。
先來(lái)看看是如何隱式返回 Promise 的,參考下面的代碼:
async function async1() {
return '秀兒';
}
console.log(async1()); // Promise {<fulfilled>: "秀兒"}
復(fù)制代碼
執(zhí)行這段代碼,可以看到調(diào)用 async 聲明的 async1 函數(shù)返回了一個(gè) Promise 對(duì)象,狀態(tài)是 resolved,返回結(jié)果如下所示:Promise {<fulfilled>: "秀兒"}。和 Promise 的鏈?zhǔn)秸{(diào)用 then 中處理返回值一樣。
await
await 需要跟 async 搭配使用,結(jié)合下面這段代碼來(lái)看看 await 到底是什么:
async function foo() {
console.log(1)
let a = await 100
console.log(a)
console.log(2)
}
console.log(0)
foo()
console.log(3)
復(fù)制代碼
站在 協(xié)程 的視角來(lái)看看這段代碼的整體執(zhí)行流程圖:

結(jié)合上圖來(lái)分析 async/await 的執(zhí)行流程:
首先,執(zhí)行 console.log(0)這個(gè)語(yǔ)句,打印出來(lái)0。緊接著就是執(zhí)行 foo函數(shù),由于foo函數(shù)是被async標(biāo)記過(guò)的,所以當(dāng)進(jìn)入該函數(shù)的時(shí)候,JS 引擎會(huì)保存當(dāng)前的調(diào)用棧等信息,然后執(zhí)行foo函數(shù)中的console.log(1)語(yǔ)句,并打印出1。當(dāng)執(zhí)行到 await 100時(shí),會(huì)默認(rèn)創(chuàng)建一個(gè)Promise對(duì)象代碼如下所示: let promise_ = new Promise((resolve,reject){ resolve(100) })在這個(gè) promise_對(duì)象創(chuàng)建的過(guò)程中,可以看到在executor函數(shù)中調(diào)用了resolve函數(shù),JS 引擎會(huì)將該任務(wù)提交給微任務(wù)隊(duì)列。然后 JS 引擎會(huì)暫停當(dāng)前協(xié)程的執(zhí)行,將主線程的控制權(quán)轉(zhuǎn)交給父協(xié)程執(zhí)行,同時(shí)會(huì)將 promise_對(duì)象返回給父協(xié)程。主線程的控制權(quán)已經(jīng)交給父協(xié)程了,這時(shí)候父協(xié)程要做的一件事是調(diào)用 promise_.then來(lái)監(jiān)控promise狀態(tài)的改變。接下來(lái)繼續(xù)執(zhí)行父協(xié)程的流程,執(zhí)行 console.log(3),并打印出來(lái)3。隨后父協(xié)程將執(zhí)行結(jié)束,在結(jié)束之前,會(huì)進(jìn)入微任務(wù)的檢查點(diǎn),然后執(zhí)行微任務(wù)隊(duì)列,微任務(wù)隊(duì)列中有 resolve(100)的任務(wù)等待執(zhí)行,執(zhí)行到這里的時(shí)候,會(huì)觸發(fā)promise_.then中的回調(diào)函數(shù),如下所示:
promise_.then((value) => {
// 回調(diào)函數(shù)被激活后
// 將主線程控制權(quán)交給foo協(xié)程,并將vaule值傳給協(xié)程
})
復(fù)制代碼
該回調(diào)函數(shù)被激活以后,會(huì)將主線程的控制權(quán)交給 foo函數(shù)的協(xié)程,并同時(shí)將value值傳給該協(xié)程。foo協(xié)程激活之后,會(huì)把剛才的value值賦給了變量a,然后foo協(xié)程繼續(xù)執(zhí)行后續(xù)語(yǔ)句,執(zhí)行完成之后,將控制權(quán)歸還給父協(xié)程。
以上就是 await/async 的執(zhí)行流程。正是因?yàn)?nbsp;async 和 await 在背后做了大量的工作,所以我們才能用同步的方式寫(xiě)出異步代碼來(lái)。
當(dāng)然也存在一些缺點(diǎn),因?yàn)?nbsp;await 將異步代碼改造成了同步代碼,如果多個(gè)異步代碼沒(méi)有依賴性卻使用了 await 會(huì)導(dǎo)致性能上的降低。
async/await總結(jié)
Promise的編程模型依然充斥著大量的then方法,雖然解決了回調(diào)地獄的問(wèn)題,但是在語(yǔ)義方面依然存在缺陷,代碼中充斥著大量的then函數(shù),這就是async/await出現(xiàn)的原因。使用 async/await可以實(shí)現(xiàn)用同步代碼的風(fēng)格來(lái)編寫(xiě)異步代碼,這是因?yàn)?nbsp;async/await的基礎(chǔ)技術(shù)使用了Generator生成器和Promise,Generator生成器是協(xié)程的實(shí)現(xiàn),利用Generator生成器能實(shí)現(xiàn)生成器函數(shù)的暫停和恢復(fù)。另外,V8 引擎還為 async/await做了大量的語(yǔ)法層面包裝,所以了解隱藏在背后的代碼有助于加深你對(duì)async/await的理解。async/await無(wú)疑是異步編程領(lǐng)域非常大的一個(gè)革新,也是未來(lái)的一個(gè)主流的編程風(fēng)格。其實(shí),除了 JavaScript,Python、Dart、C# 等語(yǔ)言也都引入了async/await,使用它不僅能讓代碼更加整潔美觀,而且還能確保該函數(shù)始終都能返回Promise。
異步編程總結(jié)
早期的異步回調(diào)函數(shù)雖然解決了同步阻塞的問(wèn)題,但是容易寫(xiě)出回調(diào)地獄。 Generator生成器最大的特點(diǎn)是可以控制函數(shù)的執(zhí)行,是協(xié)程的一種實(shí)現(xiàn)方式。Promise的更多內(nèi)容可以看我的這篇文章:《異步編程 Promise:從使用到手寫(xiě)實(shí)現(xiàn)(4200字長(zhǎng)文)》:https://juejin.cn/post/6978419919582920740async/await可以算是異步編程的終極解決方案,它通過(guò)同步的方式寫(xiě)異步代碼,可以把await看作是讓出線程的標(biāo)志,先去執(zhí)行async函數(shù)外部的代碼,等調(diào)用棧為空再回來(lái)調(diào)用await后面的代碼。
關(guān)于本文
來(lái)源:起風(fēng)了Q
https://juejin.cn/post/6978689182809997320
