JavaScript引擎實現async/await的方法實例
前言
我們都知道Promise 能很好地解決回調地獄的問題,但是這種方式充滿了 Promise 的 then() 方法,如果處理流程比較復雜的話,那么整段代碼將充斥著 then,語義化不明顯,代碼不能很好地表示執(zhí)行流程,使用 promise.then 也是相當復雜,雖然整個請求流程已經線性化了,但是代碼里面包含了大量的 then 函數,使得代碼依然不是太容易閱讀。基于這個原因,ES7 引入了 async/await,這是 JavaScript 異步編程的一個重大改進,提供了在不阻塞主線程的情況下使用同步代碼實現異步訪問資源的能力,并且使得代碼邏輯更加清晰。
JavaScript 引擎是如何實現 async/await 的。如果上來直接介紹 async/await 的使用方式的話,那么你可能會有點懵,所以我們就從其最底層的技術點一步步往上講解,從而帶你徹底弄清楚 async 和 await 到底是怎么工作的。
首先介紹生成器(Generator)是如何工作的,接著講解 Generator 的底層實現機制——協程(Coroutine);又因為 async/await 使用了 Generator 和 Promise 兩種技術,所以緊接著我們就通過 Generator 和 Promise 來分析 async/await 到底是如何以同步的方式來編寫異步代碼的。
生成器 VS 協程
生成器函數是一個帶星號函數,而且是可以暫停執(zhí)行和恢復執(zhí)行的。
function* genDemo() {
console.log("開始執(zhí)行第一段")
yield 'generator 2'
console.log("開始執(zhí)行第二段")
yield 'generator 2'
console.log("開始執(zhí)行第三段")
yield 'generator 2'
console.log("執(zhí)行結束")
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')執(zhí)行上面這段代碼,觀察輸出結果,你會發(fā)現函數 genDemo 并不是一次執(zhí)行完的,全局代碼和 genDemo 函數交替執(zhí)行。其實這就是生成器函數的特性,可以暫停執(zhí)行,也可以恢復執(zhí)行。下面我們就來看看生成器函數的具體使用方式:
- 在生成器函數內部執(zhí)行一段代碼,如果遇到 yield 關鍵字,那么 JavaScript 引擎將返回關鍵字后面的內容給外部,并暫停該函數的執(zhí)行。
- 外部函數可以通過 next 方法恢復函數的執(zhí)行。
關于函數的暫停和恢復,相信你一定很好奇這其中的原理,那么接下來我們就來簡單介紹下 JavaScript 引擎 V8 是如何實現一個函數的暫停和恢復的,這也會有助于你理解后面要介紹的 async/await。
要搞懂函數為何能暫停和恢復,那你首先要了解協程的概念。協程是一種比線程更加輕量級的存在。你可以把協程看成是跑在線程上的任務,一個線程上可以存在多個協程,但是在線程上同時只能執(zhí)行一個協程,比如當前執(zhí)行的是 A 協程,要啟動 B 協程,那么 A 協程就需要將主線程的控制權交給 B 協程,這就體現在 A 協程暫停執(zhí)行,B 協程恢復執(zhí)行;同樣,也可以從 B 協程中啟動 A 協程。通常,如果從 A 協程啟動 B 協程,我們就把 A 協程稱為 B 協程的父協程。
正如一個進程可以擁有多個線程一樣,一個線程也可以擁有多個協程。最重要的是,協程不是被操作系統內核所管理,而完全是由程序所控制(也就是在用戶態(tài)執(zhí)行)。這樣帶來的好處就是性能得到了很大的提升,不會像線程切換那樣消耗資源。
為了讓你更好地理解協程是怎么執(zhí)行的,我結合上面那段代碼的執(zhí)行過程,畫出了下面的“協程執(zhí)行流程圖”,你可以對照著代碼來分析:

從圖中可以看出來協程的四點規(guī)則:
- 通過調用生成器函數 genDemo 來創(chuàng)建一個協程 gen,創(chuàng)建之后,gen 協程并沒有立即執(zhí)行。
- 要讓 gen 協程執(zhí)行,需要通過調用 gen.next。
- 當協程正在執(zhí)行的時候,可以通過 yield 關鍵字來暫停 gen 協程的執(zhí)行,并返回主要信息給父協程。
- 如果協程在執(zhí)行期間,遇到了 return 關鍵字,那么 JavaScript 引擎會結束當前協程,并將 return 后面的內容返回給父協程。
不過,對于上面這段代碼,你可能又有這樣疑問:父協程有自己的調用棧,gen 協程時也有自己的調用棧,當 gen 協程通過 yield 把控制權交給父協程時,V8 是如何切換到父協程的調用棧?當父協程通過 gen.next 恢復 gen 協程時,又是如何切換 gen 協程的調用棧?
要搞清楚上面的問題,你需要關注以下兩點內容。
第一點:gen 協程和父協程是在主線程上交互執(zhí)行的,并不是并發(fā)執(zhí)行的,它們之前的切換是通過 yield 和 gen.next 來配合完成的。
第二點:當在 gen 協程中調用了 yield 方法時,JavaScript 引擎會保存 gen 協程當前的調用棧信息,并恢復父協程的調用棧信息。同樣,當在父協程中執(zhí)行 gen.next 時,JavaScript 引擎會保存父協程的調用棧信息,并恢復 gen 協程的調用棧信息。
為了直觀理解父協程和 gen 協程是如何切換調用棧的

到這里相信你已經弄清楚了協程是怎么工作的,其實在 JavaScript 中,生成器就是協程的一種實現方式,這樣相信你也就理解什么是生成器了。那么接下來,我們使用生成器和 Promise 來改造開頭的那段 Promise 代碼。改造后的代碼如下所示:
//foo函數
function* foo() {
let response1 = yield fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}
//執(zhí)行foo函數的代碼
let gen = foo()
function getGenPromise(gen) {
return gen.next().value
}
getGenPromise(gen).then((response) => {
console.log('response1')
console.log(response)
return getGenPromise(gen)
}).then((response) => {
console.log('response2')
console.log(response)
})從圖中可以看到,foo 函數是一個生成器函數,在 foo 函數里面實現了用同步代碼形式來實現異步操作;但是在 foo 函數外部,我們還需要寫一段執(zhí)行 foo 函數的代碼,如上述代碼的后半部分所示,那下面我們就來分析下這段代碼是如何工作的。
- 首先執(zhí)行的是let gen = foo(),創(chuàng)建了 gen 協程。然后在父協程中通過執(zhí)行 gen.next 把主線程的控制權交給 gen 協程。
- gen 協程獲取到主線程的控制權后,就調用 fetch 函數創(chuàng)建了一個 Promise 對象 response1,然后通過 yield 暫停 gen 協程的執(zhí)行,并將 response1 返回給父協程。
- 父協程恢復執(zhí)行后,調用 response1.then 方法等待請求結果。
- 等通過 fetch 發(fā)起的請求完成之后,會調用 then 中的回調函數,then 中的回調函數拿到結果之后,通過調用 gen.next 放棄主線程的控制權,將控制權交 gen 協程繼續(xù)執(zhí)行下個請求。
以上就是協程和 Promise 相互配合執(zhí)行的一個大致流程。不過通常,我們把執(zhí)行生成器的代碼封裝成一個函數,并把這個執(zhí)行生成器代碼的函數稱為執(zhí)行器(可參考著名的 co 框架),如下面這種方式:
function* foo() {
let response1 = yield fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}
co(foo());通過使用生成器配合執(zhí)行器,就能實現使用同步的方式寫出異步代碼了,這樣也大大加強了代碼的可讀性。
async/await
雖然生成器已經能很好地滿足我們的需求了,但是程序員的追求是無止境的,這不又在 ES7 中引入了 async/await,這種方式能夠徹底告別執(zhí)行器和生成器,實現更加直觀簡潔的代碼。其實 async/await 技術背后的秘密就是 Promise 和生成器應用,往低層說就是微任務和協程應用。要搞清楚 async 和 await 的工作原理,我們就得對 async 和 await 分開分析。
async
我們先來看看 async 到底是什么?根據 MDN 定義,async 是一個通過異步執(zhí)行并隱式返回 Promise 作為結果的函數。
這里我們先來看看是如何隱式返回 Promise 的,你可以參考下面的代碼:
async function foo() {
return 2
}
console.log(foo()) // Promise {<resolved>: 2}執(zhí)行這段代碼,我們可以看到調用 async 聲明的 foo 函數返回了一個 Promise 對象,狀態(tài)是 resolved,返回結果如下所示:
Promise {<resolved>: 2}await
我們知道了 async 函數返回的是一個 Promise 對象,那下面我們再結合文中這段代碼來看看 await 到底是什么。
async function foo() {
console.log(1)
let a = await 100
console.log(a)
console.log(2)
}
console.log(0)
foo()
console.log(3)觀察上面這段代碼,你能判斷出打印出來的內容是什么嗎?這得先來分析 async 結合 await 到底會發(fā)生什么。在詳細介紹之前,我們先站在協程的視角來看看這段代碼的整體執(zhí)行流程圖:

結合上圖,我們來一起分析下 async/await 的執(zhí)行流程。
首先,執(zhí)行console.log(0)這個語句,打印出來 0。
緊接著就是執(zhí)行 foo 函數,由于 foo 函數是被 async 標記過的,所以當進入該函數的時候,JavaScript 引擎會保存當前的調用棧等信息,然后執(zhí)行 foo 函數中的console.log(1)語句,并打印出 1。
接下來就執(zhí)行到 foo 函數中的await 100這個語句了,這里是我們分析的重點,因為在執(zhí)行await 100這個語句時,JavaScript 引擎在背后為我們默默做了太多的事情,那么下面我們就把這個語句拆開,來看看 JavaScript 到底都做了哪些事情。
當執(zhí)行到await 100時,會默認創(chuàng)建一個 Promise 對象,代碼如下所示
let promise_ = new Promise((resolve,reject){
resolve(100)
})在這個 promise_ 對象創(chuàng)建的過程中,我們可以看到在 executor 函數中調用了 resolve 函數,JavaScript 引擎會將該任務提交給微任務隊列。
然后 JavaScript 引擎會暫停當前協程的執(zhí)行,將主線程的控制權轉交給父協程執(zhí)行,同時會將 promise_ 對象返回給父協程。
主線程的控制權已經交給父協程了,這時候父協程要做的一件事是調用 promise_.then 來監(jiān)控 promise 狀態(tài)的改變。接下來繼續(xù)執(zhí)行父協程的流程,這里我們執(zhí)行console.log(3),并打印出來 3。
隨后父協程將執(zhí)行結束,在結束之前,會進入微任務的檢查點,然后執(zhí)行微任務隊列,微任務隊列中有resolve(100)的任務等待執(zhí)行,執(zhí)行到這里的時候,會觸發(fā) promise_.then 中的回調函數,如下所示:
promise_.then((value)=>{
//回調函數被激活后
//將主線程控制權交給foo協程,并將vaule值傳給協程
})該回調函數被激活以后,會將主線程的控制權交給 foo 函數的協程,并同時將 value 值傳給該協程。
foo 協程激活之后,會把剛才的 value 值賦給了變量 a,然后 foo 協程繼續(xù)執(zhí)行后續(xù)語句,執(zhí)行完成之后,將控制權歸還給父協程。
以上就是 await/async 的執(zhí)行流程。正是因為 async 和 await 在背后為我們做了大量的工作,所以我們才能用同步的方式寫出異步代碼來。
小結
Promise 的編程模型依然充斥著大量的 then 方法,雖然解決了回調地獄的問題,但是在語義方面依然存在缺陷,代碼中充斥著大量的 then 函數,這就是 async/await 出現的原因。
使用 async/await 可以實現用同步代碼的風格來編寫異步代碼,這是因為 async/await 的基礎技術使用了生成器和 Promise,生成器是協程的實現,利用生成器能實現生成器函數的暫停和恢復。
另外,V8 引擎還為 async/await 做了大量的語法層面包裝,所以了解隱藏在背后的代碼有助于加深你對 async/await 的理解。async/await 無疑是異步編程領域非常大的一個革新,也是未來的一個主流的編程風格。
其實,除了 JavaScript,Python、Dart、C# 等語言也都引入了 async/await,使用它不僅能讓代碼更加整潔美觀,而且還能確保該函數始終都能返回 Promise。
總結
到此這篇關于JavaScript引擎實現async/await的文章就介紹到這了,更多相關js實現async/await內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
純JS實現的讀取excel文件內容功能示例【支持所有瀏覽器】
這篇文章主要介紹了純JS實現的讀取excel文件內容功能,結合實例形式分析了基于js相關插件進行Excel文件讀取的相關操作技巧,需要的朋友可以參考下2018-06-06

