JavaScript引擎實現(xiàn)async/await的方法實例
前言
我們都知道Promise 能很好地解決回調(diào)地獄的問題,但是這種方式充滿了 Promise 的 then() 方法,如果處理流程比較復(fù)雜的話,那么整段代碼將充斥著 then,語義化不明顯,代碼不能很好地表示執(zhí)行流程,使用 promise.then 也是相當(dāng)復(fù)雜,雖然整個請求流程已經(jīng)線性化了,但是代碼里面包含了大量的 then 函數(shù),使得代碼依然不是太容易閱讀?;谶@個原因,ES7 引入了 async/await,這是 JavaScript 異步編程的一個重大改進,提供了在不阻塞主線程的情況下使用同步代碼實現(xiàn)異步訪問資源的能力,并且使得代碼邏輯更加清晰。
JavaScript 引擎是如何實現(xiàn) async/await 的。如果上來直接介紹 async/await 的使用方式的話,那么你可能會有點懵,所以我們就從其最底層的技術(shù)點一步步往上講解,從而帶你徹底弄清楚 async 和 await 到底是怎么工作的。
首先介紹生成器(Generator)是如何工作的,接著講解 Generator 的底層實現(xiàn)機制——協(xié)程(Coroutine);又因為 async/await 使用了 Generator 和 Promise 兩種技術(shù),所以緊接著我們就通過 Generator 和 Promise 來分析 async/await 到底是如何以同步的方式來編寫異步代碼的。
生成器 VS 協(xié)程
生成器函數(shù)是一個帶星號函數(shù),而且是可以暫停執(zhí)行和恢復(fù)執(zhí)行的。
function* genDemo() { console.log("開始執(zhí)行第一段") yield 'generator 2' console.log("開始執(zhí)行第二段") yield 'generator 2' console.log("開始執(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')
執(zhí)行上面這段代碼,觀察輸出結(jié)果,你會發(fā)現(xiàn)函數(shù) genDemo 并不是一次執(zhí)行完的,全局代碼和 genDemo 函數(shù)交替執(zhí)行。其實這就是生成器函數(shù)的特性,可以暫停執(zhí)行,也可以恢復(fù)執(zhí)行。下面我們就來看看生成器函數(shù)的具體使用方式:
- 在生成器函數(shù)內(nèi)部執(zhí)行一段代碼,如果遇到 yield 關(guān)鍵字,那么 JavaScript 引擎將返回關(guān)鍵字后面的內(nèi)容給外部,并暫停該函數(shù)的執(zhí)行。
- 外部函數(shù)可以通過 next 方法恢復(fù)函數(shù)的執(zhí)行。
關(guān)于函數(shù)的暫停和恢復(fù),相信你一定很好奇這其中的原理,那么接下來我們就來簡單介紹下 JavaScript 引擎 V8 是如何實現(xiàn)一個函數(shù)的暫停和恢復(fù)的,這也會有助于你理解后面要介紹的 async/await。
要搞懂函數(shù)為何能暫停和恢復(fù),那你首先要了解協(xié)程的概念。協(xié)程是一種比線程更加輕量級的存在。你可以把協(xié)程看成是跑在線程上的任務(wù),一個線程上可以存在多個協(xié)程,但是在線程上同時只能執(zhí)行一個協(xié)程,比如當(dāng)前執(zhí)行的是 A 協(xié)程,要啟動 B 協(xié)程,那么 A 協(xié)程就需要將主線程的控制權(quán)交給 B 協(xié)程,這就體現(xiàn)在 A 協(xié)程暫停執(zhí)行,B 協(xié)程恢復(fù)執(zhí)行;同樣,也可以從 B 協(xié)程中啟動 A 協(xié)程。通常,如果從 A 協(xié)程啟動 B 協(xié)程,我們就把 A 協(xié)程稱為 B 協(xié)程的父協(xié)程。
正如一個進程可以擁有多個線程一樣,一個線程也可以擁有多個協(xié)程。最重要的是,協(xié)程不是被操作系統(tǒng)內(nèi)核所管理,而完全是由程序所控制(也就是在用戶態(tài)執(zhí)行)。這樣帶來的好處就是性能得到了很大的提升,不會像線程切換那樣消耗資源。
為了讓你更好地理解協(xié)程是怎么執(zhí)行的,我結(jié)合上面那段代碼的執(zhí)行過程,畫出了下面的“協(xié)程執(zhí)行流程圖”,你可以對照著代碼來分析:
從圖中可以看出來協(xié)程的四點規(guī)則:
- 通過調(diào)用生成器函數(shù) genDemo 來創(chuàng)建一個協(xié)程 gen,創(chuàng)建之后,gen 協(xié)程并沒有立即執(zhí)行。
- 要讓 gen 協(xié)程執(zhí)行,需要通過調(diào)用 gen.next。
- 當(dāng)協(xié)程正在執(zhí)行的時候,可以通過 yield 關(guān)鍵字來暫停 gen 協(xié)程的執(zhí)行,并返回主要信息給父協(xié)程。
- 如果協(xié)程在執(zhí)行期間,遇到了 return 關(guān)鍵字,那么 JavaScript 引擎會結(jié)束當(dāng)前協(xié)程,并將 return 后面的內(nèi)容返回給父協(xié)程。
不過,對于上面這段代碼,你可能又有這樣疑問:父協(xié)程有自己的調(diào)用棧,gen 協(xié)程時也有自己的調(diào)用棧,當(dāng) gen 協(xié)程通過 yield 把控制權(quán)交給父協(xié)程時,V8 是如何切換到父協(xié)程的調(diào)用棧?當(dāng)父協(xié)程通過 gen.next 恢復(fù) gen 協(xié)程時,又是如何切換 gen 協(xié)程的調(diào)用棧?
要搞清楚上面的問題,你需要關(guān)注以下兩點內(nèi)容。
第一點:gen 協(xié)程和父協(xié)程是在主線程上交互執(zhí)行的,并不是并發(fā)執(zhí)行的,它們之前的切換是通過 yield 和 gen.next 來配合完成的。
第二點:當(dāng)在 gen 協(xié)程中調(diào)用了 yield 方法時,JavaScript 引擎會保存 gen 協(xié)程當(dāng)前的調(diào)用棧信息,并恢復(fù)父協(xié)程的調(diào)用棧信息。同樣,當(dāng)在父協(xié)程中執(zhí)行 gen.next 時,JavaScript 引擎會保存父協(xié)程的調(diào)用棧信息,并恢復(fù) gen 協(xié)程的調(diào)用棧信息。
為了直觀理解父協(xié)程和 gen 協(xié)程是如何切換調(diào)用棧的
到這里相信你已經(jīng)弄清楚了協(xié)程是怎么工作的,其實在 JavaScript 中,生成器就是協(xié)程的一種實現(xiàn)方式,這樣相信你也就理解什么是生成器了。那么接下來,我們使用生成器和 Promise 來改造開頭的那段 Promise 代碼。改造后的代碼如下所示:
//foo函數(shù) 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函數(shù)的代碼 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 函數(shù)是一個生成器函數(shù),在 foo 函數(shù)里面實現(xiàn)了用同步代碼形式來實現(xiàn)異步操作;但是在 foo 函數(shù)外部,我們還需要寫一段執(zhí)行 foo 函數(shù)的代碼,如上述代碼的后半部分所示,那下面我們就來分析下這段代碼是如何工作的。
- 首先執(zhí)行的是let gen = foo(),創(chuàng)建了 gen 協(xié)程。然后在父協(xié)程中通過執(zhí)行 gen.next 把主線程的控制權(quán)交給 gen 協(xié)程。
- gen 協(xié)程獲取到主線程的控制權(quán)后,就調(diào)用 fetch 函數(shù)創(chuàng)建了一個 Promise 對象 response1,然后通過 yield 暫停 gen 協(xié)程的執(zhí)行,并將 response1 返回給父協(xié)程。
- 父協(xié)程恢復(fù)執(zhí)行后,調(diào)用 response1.then 方法等待請求結(jié)果。
- 等通過 fetch 發(fā)起的請求完成之后,會調(diào)用 then 中的回調(diào)函數(shù),then 中的回調(diào)函數(shù)拿到結(jié)果之后,通過調(diào)用 gen.next 放棄主線程的控制權(quán),將控制權(quán)交 gen 協(xié)程繼續(xù)執(zhí)行下個請求。
以上就是協(xié)程和 Promise 相互配合執(zhí)行的一個大致流程。不過通常,我們把執(zhí)行生成器的代碼封裝成一個函數(shù),并把這個執(zhí)行生成器代碼的函數(shù)稱為執(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í)行器,就能實現(xiàn)使用同步的方式寫出異步代碼了,這樣也大大加強了代碼的可讀性。
async/await
雖然生成器已經(jīng)能很好地滿足我們的需求了,但是程序員的追求是無止境的,這不又在 ES7 中引入了 async/await,這種方式能夠徹底告別執(zhí)行器和生成器,實現(xiàn)更加直觀簡潔的代碼。其實 async/await 技術(shù)背后的秘密就是 Promise 和生成器應(yīng)用,往低層說就是微任務(wù)和協(xié)程應(yīng)用。要搞清楚 async 和 await 的工作原理,我們就得對 async 和 await 分開分析。
async
我們先來看看 async 到底是什么?根據(jù) MDN 定義,async 是一個通過異步執(zhí)行并隱式返回 Promise 作為結(jié)果的函數(shù)。
這里我們先來看看是如何隱式返回 Promise 的,你可以參考下面的代碼:
async function foo() { return 2 } console.log(foo()) // Promise {<resolved>: 2}
執(zhí)行這段代碼,我們可以看到調(diào)用 async 聲明的 foo 函數(shù)返回了一個 Promise 對象,狀態(tài)是 resolved,返回結(jié)果如下所示:
Promise {<resolved>: 2}
await
我們知道了 async 函數(shù)返回的是一個 Promise 對象,那下面我們再結(jié)合文中這段代碼來看看 await 到底是什么。
async function foo() { console.log(1) let a = await 100 console.log(a) console.log(2) } console.log(0) foo() console.log(3)
觀察上面這段代碼,你能判斷出打印出來的內(nèi)容是什么嗎?這得先來分析 async 結(jié)合 await 到底會發(fā)生什么。在詳細介紹之前,我們先站在協(xié)程的視角來看看這段代碼的整體執(zhí)行流程圖:
結(jié)合上圖,我們來一起分析下 async/await 的執(zhí)行流程。
首先,執(zhí)行console.log(0)這個語句,打印出來 0。
緊接著就是執(zhí)行 foo 函數(shù),由于 foo 函數(shù)是被 async 標(biāo)記過的,所以當(dāng)進入該函數(shù)的時候,JavaScript 引擎會保存當(dāng)前的調(diào)用棧等信息,然后執(zhí)行 foo 函數(shù)中的console.log(1)語句,并打印出 1。
接下來就執(zhí)行到 foo 函數(shù)中的await 100這個語句了,這里是我們分析的重點,因為在執(zhí)行await 100這個語句時,JavaScript 引擎在背后為我們默默做了太多的事情,那么下面我們就把這個語句拆開,來看看 JavaScript 到底都做了哪些事情。
當(dāng)執(zhí)行到await 100時,會默認創(chuàng)建一個 Promise 對象,代碼如下所示
let promise_ = new Promise((resolve,reject){ resolve(100) })
在這個 promise_ 對象創(chuàng)建的過程中,我們可以看到在 executor 函數(shù)中調(diào)用了 resolve 函數(shù),JavaScript 引擎會將該任務(wù)提交給微任務(wù)隊列。
然后 JavaScript 引擎會暫停當(dāng)前協(xié)程的執(zhí)行,將主線程的控制權(quán)轉(zhuǎn)交給父協(xié)程執(zhí)行,同時會將 promise_ 對象返回給父協(xié)程。
主線程的控制權(quán)已經(jīng)交給父協(xié)程了,這時候父協(xié)程要做的一件事是調(diào)用 promise_.then 來監(jiān)控 promise 狀態(tài)的改變。接下來繼續(xù)執(zhí)行父協(xié)程的流程,這里我們執(zhí)行console.log(3),并打印出來 3。
隨后父協(xié)程將執(zhí)行結(jié)束,在結(jié)束之前,會進入微任務(wù)的檢查點,然后執(zhí)行微任務(wù)隊列,微任務(wù)隊列中有resolve(100)的任務(wù)等待執(zhí)行,執(zhí)行到這里的時候,會觸發(fā) promise_.then 中的回調(diào)函數(shù),如下所示:
promise_.then((value)=>{ //回調(diào)函數(shù)被激活后 //將主線程控制權(quán)交給foo協(xié)程,并將vaule值傳給協(xié)程 })
該回調(diào)函數(shù)被激活以后,會將主線程的控制權(quán)交給 foo 函數(shù)的協(xié)程,并同時將 value 值傳給該協(xié)程。
foo 協(xié)程激活之后,會把剛才的 value 值賦給了變量 a,然后 foo 協(xié)程繼續(xù)執(zhí)行后續(xù)語句,執(zhí)行完成之后,將控制權(quán)歸還給父協(xié)程。
以上就是 await/async 的執(zhí)行流程。正是因為 async 和 await 在背后為我們做了大量的工作,所以我們才能用同步的方式寫出異步代碼來。
小結(jié)
Promise 的編程模型依然充斥著大量的 then 方法,雖然解決了回調(diào)地獄的問題,但是在語義方面依然存在缺陷,代碼中充斥著大量的 then 函數(shù),這就是 async/await 出現(xiàn)的原因。
使用 async/await 可以實現(xiàn)用同步代碼的風(fēng)格來編寫異步代碼,這是因為 async/await 的基礎(chǔ)技術(shù)使用了生成器和 Promise,生成器是協(xié)程的實現(xiàn),利用生成器能實現(xiàn)生成器函數(shù)的暫停和恢復(fù)。
另外,V8 引擎還為 async/await 做了大量的語法層面包裝,所以了解隱藏在背后的代碼有助于加深你對 async/await 的理解。async/await 無疑是異步編程領(lǐng)域非常大的一個革新,也是未來的一個主流的編程風(fēng)格。
其實,除了 JavaScript,Python、Dart、C# 等語言也都引入了 async/await,使用它不僅能讓代碼更加整潔美觀,而且還能確保該函數(shù)始終都能返回 Promise。
總結(jié)
到此這篇關(guān)于JavaScript引擎實現(xiàn)async/await的文章就介紹到這了,更多相關(guān)js實現(xiàn)async/await內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js Firefox 加入收藏夾功能代碼 兼容Firefox 和 IE
最近改用Firefox后,發(fā)現(xiàn)很多網(wǎng)站的“加入收藏”鏈接點擊無效了,后來發(fā)現(xiàn)原來是IE瀏覽器和Firefox瀏覽器的“加入收藏夾”的寫法是不同的。2009-12-12Bootstrapvalidator校驗、校驗清除重置的實現(xiàn)代碼(推薦)
這篇文章給大家介紹了bootstrapvalidator校驗、校驗清除重置的實現(xiàn)代碼,在代碼中需要我們引入css與js文件,大家可以參考下文的代碼2016-09-09JavaScript學(xué)習(xí)筆記之基于定時器實現(xiàn)圖片無縫滾動功能詳解
這篇文章主要介紹了JavaScript學(xué)習(xí)筆記之基于定時器實現(xiàn)圖片無縫滾動功能,結(jié)合實例形式分析了javascript定時器與頁面元素屬性動態(tài)設(shè)置等相關(guān)操作技巧,需要的朋友可以參考下2019-01-01javaScript強制保留兩位小數(shù)的輸入數(shù)校驗和小數(shù)保留問題
這篇文章主要介紹了javaScript強制保留兩位小數(shù)的輸入數(shù)校驗和小數(shù)保留問題,非常不錯,具有一定的參考借鑒價值,需要的朋友參考下吧2018-05-05