可視化的JavaScript動態(tài)圖演示 Promises & Async/Await 的過程
原由
你是否運行過不按你預(yù)期運行的 js 代碼 ?
比如:某個函數(shù)被隨機的、不可預(yù)測時間的執(zhí)行了,或者被延遲執(zhí)行了。
這時,你需要從 ES6 中引入的一個非??岬男绿匦? Promise 來處理你的問題。
為了深入理解 Promise ,我在某個不眠之夜,做了一些動畫來演示 Promise 的運行,我多年來的好奇心終于得到實現(xiàn)。
對于 Promise ,您為什么要使用它,它在底層是如何工作的,以及我們?nèi)绾我宰瞵F(xiàn)代的方式編寫它呢?
介紹
在書寫 JavaScript 的時候,我們經(jīng)常不得不去處理一些依賴于其它任務(wù)的任務(wù)!
比如:我們想要得到一個圖片,對其進行壓縮,應(yīng)用一個濾鏡,然后保存它 。
首先,先用 getImage 函數(shù)要得到我們想要編輯的圖片。
一旦圖片被成功加載,把這個圖片值傳到一個 ocmpressImage 函數(shù)中。
當圖片已經(jīng)被成功地重新調(diào)整大小后,在 applyFilter 函數(shù)中為圖片應(yīng)用一個濾鏡。
在圖片被壓縮和添加濾鏡后,保存圖片并且打印成功的日志!
最后,代碼很簡單如圖:

注意到了嗎?盡管以上代碼也能得到我們想要的結(jié)果,但是完成的過程并不是友好。
使用了大量嵌套的回調(diào)函數(shù),這使我們的代碼閱讀起來特別困難。
因為寫了許多嵌套的回調(diào)函數(shù),這些回調(diào)函數(shù)又依賴于前一個回調(diào)函數(shù),這通常被稱為 回調(diào)地獄。
幸運的,ES6 中的 Promise 可能很好的處理這種情況!
讓我們看看 promise 是什么,以及它是如何在類似于上述的情況下幫助我們的。
Promise語法
ES6引入了Promise。在許多教程中,你可能會讀到這樣的內(nèi)容:
Promise 是一個值的占位符,這個值在未來的某個時間要么 resolve 要么 reject 。
對于我來說,這樣的解釋從沒有讓事情變得更清楚。
事實上,它只是讓我感覺 Promise 是一個奇怪的、模糊的、不可預(yù)測的一段魔法。
接下來讓我們看看 promise 真正是什么?
我們可以使用一個接收一個回調(diào)函數(shù)的 Promise 構(gòu)造器創(chuàng)建一個 promise。
好酷,讓我們嘗試一下!

等等,剛剛得到的返回只是什么?
Promise 是一個對象,它包含一個狀態(tài) PromiseStatus 和一個值 PromiseValue。
在上面的例子中,你可以看到 PromiseStatus 的值是 pending, PromiseValue 的值是 undefined。
不過 - 你將永遠不會與這個對象進行交互,你甚至不能訪問 PromiseStatus 和 PromiseValue 這兩個屬性!
然而,在使用 Promise 的時候,這兩個屬性的值是非常重要的。
PromiseStatus 的值,也就是 Promise 的狀態(tài),可以是以下三個值之一:
- ✅ fulfilled: promise 已經(jīng)被 resolved。一切都很好,在 promise 內(nèi)部沒有錯誤發(fā)生。
- ❌ rejected: promise 已經(jīng)被 rejected。哎呦,某些事情出錯了。
- ⏳ pending: promise 暫時還沒有被解決也沒有被拒絕,仍然處于 pending 狀態(tài)
好吧,這一切聽起來很棒,但是什么時候 promise 的狀態(tài)是 pending、fulfilled 或 rejected 呢? 為什么這個狀態(tài)很重要呢?
在上面的例子中,我們只是為 Promise構(gòu)造器傳遞了一個簡單的回調(diào)函數(shù) () => {} 。
然而,這個回調(diào)函數(shù)實際上接受兩個參數(shù)。
- 第一個參數(shù)的值經(jīng)常被叫做 resolve 或 res,它是一個函數(shù),在 Promise 應(yīng)該解決 resolve 的時候會被調(diào)用。
- 第二個參數(shù)的值經(jīng)常被叫做 reject 或rej,它也是一個函數(shù),在 Promise 出現(xiàn)一些錯誤應(yīng)該被拒絕 reject 的時候被調(diào)用。

讓我們嘗試看看當我們調(diào)用 resolve 或 reject 方法時得到的日志。
在我的例子中,把 resolve 方法叫做 res,把 reject 方法叫做 rej。

太好了!我們終于知道如何擺脫 pending 狀態(tài)和 undefined 值了!
- 當我們調(diào)用 resolve 方法時,promise 的狀態(tài)是 fulfilled。
- 當我們調(diào)用 reject 方法時,promise 的狀態(tài)是 rejected。
有趣的是,我讓(Jake Archibald)校對了這篇文章,他實際上指出 Chrome 中存在一個錯誤,該錯誤當前將狀態(tài)顯示為 “ fulfilled” 而不是 “ resolved”。感謝 Mathias Bynens,它現(xiàn)已在Canary 中修復(fù)!
好了,現(xiàn)在我們知道如何更好控制那個模糊的 Promise 對象。但是他被用來做什么呢?
在前面的介紹章節(jié),我展示了一個獲得圖片、壓縮圖片、為圖片應(yīng)用過濾器并保存它的例子!最終,這變成了一個混亂的嵌套回調(diào)。
幸運的,Promise 可以幫助我們解決這個問題!
首先,讓我們重寫整個代碼塊,以便每個函數(shù)返回一個 Promise 來代替之前的函數(shù)。
如果圖片被加載完成并且一切正常,讓我們用加載完的圖片解決 (resolve)promise。
否則,如果在加載文件時某個地方有一個錯誤,我們將會用發(fā)生的錯誤拒絕 (reject)promise 。

讓我們看下當我們在終端運行這段代碼時會發(fā)生什么?

非??幔【拖裎覀兯谕囊粯?,promise 得到了解析數(shù)據(jù)后的值。
但是現(xiàn)在呢?我們不關(guān)心整個 promise 對象,我們只關(guān)心數(shù)據(jù)的值!幸運的,有內(nèi)置的方法來得到 promise 的值。
對于一個 promise,我們可以使用它上面的 3 個方法:
- .then(): 在一個 promise 被 resolved 后調(diào)用
- .catch(): 在一個 promise 被 rejected 后被調(diào)用
- .finally(): 不論 promise 是被 resolved 還是 reject 總是調(diào)用

.then 方法接收傳遞給 resolve 方法的值。

.catch 方法接收傳遞給 rejected 方法的值。

最終,我們擁有了 promise 被解決后 (resolved) 的值,并不需要整個 promise 對象!
現(xiàn)在我們可以用這個值做任何我們想做的事。
順便提醒一下,當你知道一個 promise 總是 resolve 或者總是 reject 的時候,你可以寫 Promise.resolve 或 Promise.reject,傳入你想要 reject 或 resolve 的 promise 的值。

在下面的例子中你將會經(jīng)??吹竭@個語法。
在 getImage 的例子中,為了運行它們,我們最終不得不嵌套多個回調(diào)。幸運的,.then 處理器可以幫助我們完成這件事!
.then 它自己的執(zhí)行結(jié)果是一個 promise。這意味著我們可以鏈接任意數(shù)量的 .then:前一個 then 回調(diào)的結(jié)果將會作為參數(shù)傳遞給下一個 then 回調(diào)!

在 getImage 示例中,為了傳遞被處理的圖片到下一個函數(shù),我們可以鏈接多個 then 回調(diào)。
相比于之前最終得到許多嵌套回調(diào),現(xiàn)在我們得到了整潔的 then 鏈。

完美!這個語法看起來已經(jīng)比之前的嵌套回調(diào)好多了。
宏任務(wù)和為任務(wù)(macrotask and microtask)
我們知道了一些如何創(chuàng)建 promise 以及如何提取出 promise 的值的方法。
讓我們?yōu)槟_本添加一些更多的代碼并且再次運行它:

等下,發(fā)生了什么?!
首先,Start! 被輸出。
好的,我們已經(jīng)看到了那一個即將到來的消息:console.log('Start!') 在最前一行輸出!
然而,第二個被打印的只是 End!,并不是 promise 被解決的值!只有在 End! 被打印之后,promise 的值才會被打印。
這里發(fā)生了什么?
我們最終看到了 promise 真正的力量!盡管 JavaScript 是單線程的,我們可以使用 Promise 添加異步任務(wù)!
等等,我們之前沒見過這種情況嗎?
在 JavaScript Event Loop 中,我們不是也可以使用瀏覽器原生的方法如 setTimeout 創(chuàng)建某類異步行為嗎?
是的!然而,在事件循環(huán)內(nèi)部,實際上有 2 種類型的隊列:宏任務(wù)(macro)隊列 (或者只是叫做 任務(wù)隊列 )和 微任務(wù)隊列。
(宏)任務(wù)隊列用于 宏任務(wù),微任務(wù)隊列用于 微任務(wù)。
那么什么是宏任務(wù),什么是微任務(wù)呢?
盡管它們比我在這里介紹的要多一些,但是最常用的已經(jīng)被展示在下面的表格中!
(Macro)task:setTimeoutsetIntervalsetImmediateMicrotask:process.nextTickPromise callbackqueueMicrotask
我們看到 Promise 在微任務(wù)列表中!當一個 Promise 解決 (resolve) 并且調(diào)用它的 then()、catch() 或 finally() 方法的時候,這些方法里的回調(diào)函數(shù)被添加到微任務(wù)隊列!
這意味著 then(),chatch() 或 finally() 方法內(nèi)的回調(diào)函數(shù)不是立即被執(zhí)行,本質(zhì)上是為我們的 JavaScript 代碼添加了一些異步行為!
那么什么時候執(zhí)行 then(),catch(),或 finally() 內(nèi)的回調(diào)呢?
事件循環(huán)給與任務(wù)不同的優(yōu)先級:
- 當前在調(diào)用棧 (call stack) 內(nèi)的所有函數(shù)會被執(zhí)行。當它們返回值的時候,會被從棧內(nèi)彈出。
- 當調(diào)用棧是空的時,所有排隊的微任務(wù)會一個接一個從微任務(wù)任務(wù)隊列中彈出進入調(diào)用棧中,然后在調(diào)用棧中被執(zhí)行!(微任務(wù)自己也能返回一個新的微任務(wù),有效地創(chuàng)建無限的微任務(wù)循環(huán) )
- 如果調(diào)用棧和微任務(wù)隊列都是空的,事件循環(huán)會檢查宏任務(wù)隊列里是否還有任務(wù)。如果宏任務(wù)中還有任務(wù),會從宏任務(wù)隊列中彈出進入調(diào)用棧,被執(zhí)行后會從調(diào)用棧中彈出!
讓我們快速地看一個簡單的例子:
- Task1: 立即被添加到調(diào)用棧中的函數(shù),比如在我們的代碼中立即調(diào)用它。
- Task2,Task3,Task4: 微任務(wù),比如 promise 中 then 方法里的回調(diào),或者用 queueMicrotask 添加的一個任務(wù)。
- Task5,Task6: 宏任務(wù),比如 setTimeout 或者 setImmediate 里的回調(diào)

首先,Task1 返回一個值并且從調(diào)用棧中彈出。然后,JavaScript 引擎檢查為任務(wù)隊列中排隊的任務(wù)。一旦微任務(wù)中所有的任務(wù)被放入調(diào)用棧并且最終被彈出,JavaScript 引擎會檢查宏任務(wù)隊列中的任務(wù),將它們彈入調(diào)用棧中并且在它們返回值的時候把它們彈出調(diào)用棧。
圖中足夠粉色的盒子是不同的任務(wù),讓我們用一些真實的代碼來使用它!

在這段代碼中,我們有宏任務(wù) setTimeout 和 微任務(wù) promise 的 then 回調(diào)。
一旦 JavaScript 引擎到達 setTimeout 函數(shù)所在的那行就會涉及到事件循環(huán)。
讓我們一步一步地運行這段代碼,看看會得到什么樣的日志!
快速提一下:在下面的例子中,我正在展示的像 console.log,setTimeout 和 Promise.resolve 等方法正在被添加到調(diào)用棧中。它們是內(nèi)部的方法實際上沒有出現(xiàn)在堆棧痕跡中,因此如果你正在使用調(diào)試器,不用擔心,你不會在任何地方見到它們。它只是在沒有添加一堆樣本文件代碼的情況下使這個概念解釋起來更加簡單。
在第一行,JavaScript 引擎遇到了 console.log() 方法,它被添加到調(diào)用棧,之后它在控制臺輸出值 Start!。console.log 函數(shù)從調(diào)用棧內(nèi)彈出,之后 JavaScript 引擎繼續(xù)執(zhí)行代碼。

JavaScript 引擎遇到了 setTimeout 方法,他被彈入調(diào)用棧中。setTimeout 是瀏覽器的原生方法:它的回調(diào)函數(shù) (() => console.log('In timeout')) 將會被添加到 Web API,直到計時器完成計時。盡管我們?yōu)橛嫊r器提供的只是 0,在它被添加到宏任務(wù)隊列 (setTimeout 是一個宏任務(wù)) 之后回調(diào)還是會被首先推入 Web API。

JavaScript 引擎遇到了 Promise.resolve 方法。Promise.resolve 被添加到調(diào)用棧。在 Promise 解決 (resolve) 值之后,它的 then 中的回調(diào)函數(shù)被添加到微任務(wù)隊列。

JavaScript 引擎看到調(diào)用棧現(xiàn)在是空的。由于調(diào)用棧是空的,它將會去檢查在微任務(wù)隊列中是否有在排隊的任務(wù)!是的,有任務(wù)在排隊,promise 的 then 中的回調(diào)函數(shù)正在等待輪到它!它被彈入調(diào)用棧,之后它輸出了 promise 被解決后( resolved )的值: 在這個例子中的字符串 Promise!。

JavaScript 引擎看到調(diào)用棧是空的,因此,如果任務(wù)在排隊的話,它將會再次去檢查微任務(wù)隊列。此時,微任務(wù)隊列完全是空的。
到了去檢查宏任務(wù)隊列的時候了:setTimeout 回調(diào)仍然在那里等待!setTimeout 被彈入調(diào)用棧?;卣{(diào)函數(shù)返回 console.log 方法,輸出了字符串 In timeout!。setTimeout 回調(diào)從調(diào)用棧中彈出。

終于,所有的事情完成了! 看起來我們之前看到的輸出最終并不是那么出乎意料。
Async/Await
ES7 引入了一個新的在 JavaScript 中添加異步行為的方式并且使 promise 用起來更加簡單!隨著 async 和 await 關(guān)鍵字的引入,我們能夠創(chuàng)建一個隱式的返回一個 promise 的 async 函數(shù)。但是,我們該怎么做呢?
之前,我們看到不管是通過輸入 new Promise(() => {}),Promise.resolve 或 Promise.reject,我們都可以顯式的使用 Promise 對象創(chuàng)建 promise。
我們現(xiàn)在能夠創(chuàng)建隱式地返回一個對象的異步函數(shù),而不是顯式地使用 Promise 對象!這意味著我們不再需要寫任何 Promise 對象了。

盡管 async 函數(shù)隱式的返回 promise 是一個非常棒的事實,但是在使用 await 關(guān)鍵字的時候才能看到 async 函數(shù)的真正力量。當我們等待 await 后的值返回一個 resolved 的 promise 時,通過 await 關(guān)鍵字,我們可以暫停異步函數(shù)。如果我們想要得到這個 resolved 的 promise 的值,就像我們之前用 then 回調(diào)那樣,我們可以為被 await 的 promise 的值賦值為變量!
這樣,我們就可以暫停一個異步函數(shù)嗎?很好,但這到底是什么意思?
當我們運行下面的代碼塊時讓我們看下發(fā)生了什么:

額,這里發(fā)生了什么呢?

首先,JavaScript 引擎遇到了 console.log。它被彈入到調(diào)用棧中,這之后 Before function! 被輸出。

然后,我們調(diào)用了異步函數(shù)myFunc(),這之后myFunc函數(shù)體運行。函數(shù)主體內(nèi)的最開始一行,我們調(diào)用了另一個console.log,這次傳入的是字符串In function!。console.log被添加到調(diào)用棧中,輸出值,然后從棧內(nèi)彈出。

函數(shù)體繼續(xù)執(zhí)行,將我們帶到第二行。最終,我們看到一個await關(guān)鍵字!
最先發(fā)生的事是被等待的值執(zhí)行:在這個例子中是函數(shù)one。它被彈入調(diào)用棧,并且最終返回一個解決狀態(tài)的promise。一旦Promise被解決并且one返回一個值,JavaScript遇到了await關(guān)鍵字。
當遇到await關(guān)鍵字的時候,異步函數(shù)被暫停。函數(shù)體的執(zhí)行被暫停,async函數(shù)中剩余的代碼會在微任務(wù)中運行而不是一個常規(guī)任務(wù)!

現(xiàn)在,因為遇到了await關(guān)鍵字,異步函數(shù)myFunc被暫停,JavaScript引擎跳出異步函數(shù),并且在異步函數(shù)被調(diào)用的執(zhí)行上下文中繼續(xù)執(zhí)行代碼:在這個例子中是全局執(zhí)行上下文!♀️

最終,沒有更多的任務(wù)在全局執(zhí)行上下文中運行!事件循環(huán)檢查看看是否有任何的微任務(wù)在排隊:是的,有!在解決了one的值以后,異步函數(shù)myFunc開始排隊。myFunc被彈入調(diào)用棧中,在它之前中斷的地方繼續(xù)運行。
變量res最終獲得了它的值,也就是one返回的promise被解決的值!我們用res的值(在這個例子中是字符串One!)調(diào)用console.log。One!被打印到控制臺并且console.log從調(diào)用棧彈出。
最終,所有的事情都完成了!你注意到async函數(shù)相比于promise的then有什么不同嗎?await關(guān)鍵字暫停了async函數(shù),然而如果我們使用then的話,Promise的主體將會繼續(xù)被執(zhí)行!
嗯,這是相當多的信息!當使用Promise的時候,如果你仍然感覺有一點不知所措,完全不用擔心。我個人認為,當使用異步JavaScript的時候,只是需要經(jīng)驗去注意模式之后便會感到自信。
當使用異步JavaScript的時候,我希望你可能遇到的“無法預(yù)料的”或“不可預(yù)測的”行為現(xiàn)在變得更有意義!
最后
外國友人技術(shù)博客的語言表達的方式和風格、與國人的還是有很大差別的啊。
明明看到有很長或者很拗口的句子的時候,我就想按自己的語言來寫一篇了
可能自己寫一篇都比翻譯的快