你是否運行過不按你預期運行的 js 代碼 ?php
好比:某個函數被隨機的、不可預測時間的執行了,或者被延遲執行了。promise
這時,你須要從 ES6 中引入的一個很是酷的新特性: Promise 來處理你的問題。瀏覽器
爲了深刻理解 Promise ,我在某個不眠之夜,作了一些動畫來演示 Promise 的運行,我多年來的好奇心終於獲得實現。app
對於 Promise ,您爲何要使用它,它在底層是如何工做的,以及咱們如何以最現代的方式編寫它呢?異步
在書寫 JavaScript 的時候,咱們常常不得不去處理一些依賴於其它任務的任務!async
好比:咱們想要獲得一個圖片,對其進行壓縮,應用一個濾鏡,而後保存它 。ide
首先,先用 getImage 函數要獲得咱們想要編輯的圖片。函數
一旦圖片被成功加載,把這個圖片值傳到一個 ocmpressImage 函數中。oop
當圖片已經被成功地從新調整大小後,在 applyFilter 函數中爲圖片應用一個濾鏡。動畫
在圖片被壓縮和添加濾鏡後,保存圖片而且打印成功的日誌!
最後,代碼很簡單如圖:
注意到了嗎?儘管以上代碼也能獲得咱們想要的結果,可是完成的過程並非友好。
使用了大量嵌套的回調函數,這使咱們的代碼閱讀起來特別困難。
由於寫了許多嵌套的回調函數,這些回調函數又依賴於前一個回調函數,這一般被稱爲 回調地獄。
幸運的,ES6 中的 Promise 的能很好的處理這種狀況!
讓咱們看看 promise 是什麼,以及它是如何在相似於上述的狀況下幫助咱們的。
ES6引入了Promise。在許多教程中,你可能會讀到這樣的內容:
Promise 是一個值的佔位符,這個值在將來的某個時間要麼 resolve 要麼 reject 。
對於我來講,這樣的解釋從沒有讓事情變得更清楚。
事實上,它只是讓我感受 Promise 是一個奇怪的、模糊的、不可預測的一段魔法。
接下來讓咱們看看 promise 真正是什麼?
咱們可使用一個接收一個回調函數的 Promise 構造器建立一個 promise。
好酷,讓咱們嘗試一下!
等等,剛剛獲得的返回值是什麼?
Promise 是一個對象,它包含一個狀態 PromiseStatus
和一個值 PromiseValue
。
在上面的例子中,你能夠看到 PromiseStatus
的值是 pending, PromiseValue
的值是 undefined。
不過 - 你將永遠不會與這個對象進行交互,你甚至不能訪問 PromiseStatus
和 PromiseValue
這兩個屬性!
然而,在使用 Promise 的時候,這倆個屬性的值是很是重要的。
PromiseStatus
的值,也就是 Promise
的狀態,能夠是如下三個值之一:
✅ fulfilled
: promise
已經被 resolved
。一切都很好,在 promise
內部沒有錯誤發生。
❌ rejected
: promise
已經被 rejected
。哎呦,某些事情出錯了。
⏳ pending
: promise
暫時尚未被解決也沒有被拒絕,仍然處於 pending
狀態
好吧,這一切聽起來很棒,可是何時 promise
的狀態是 pending
、fulfilled
或 rejected
呢? 爲何這個狀態很重要呢?
在上面的例子中,咱們只是爲 Promise
構造器傳遞了一個簡單的回調函數 () => {}
。
然而,這個回調函數實際上接受兩個參數。
第一個參數的值常常被叫作 resolve
或 res
,它是一個函數,在 Promise
應該解決 resolve
的時候會被調用。
第二個參數的值常常被叫作 reject
或rej
,它也是一個函數,在 Promise
出現一些錯誤應該被拒絕 reject
的時候被調用。
讓咱們嘗試看看當咱們調用 resolve
或 reject
方法時獲得的日誌。
在個人例子中,把 resolve
方法叫作 res
,把 reject
方法叫作 rej
。
太好了!咱們終於知道如何擺脫 pending
狀態和 undefined
值了!
當咱們調用 resolve
方法時,promise
的狀態是 fulfilled
。
當咱們調用 reject
方法時,promise
的狀態是 rejected
。
有趣的是,我讓(Jake Archibald)校對了這篇文章,他實際上指出 Chrome 中存在一個錯誤,該錯誤當前將狀態顯示爲 「 fulfilled」 而不是 「 resolved」。感謝 Mathias Bynens,它現已在Canary 中修復!????????????
好了,如今咱們知道如何更好控制那個模糊的 Promise 對象。可是他被用來作什麼呢?
在前面的介紹章節,我展現了一個得到圖片、壓縮圖片、爲圖片應用過濾器並保存它的例子!最終,這變成了一個混亂的嵌套回調。
幸運的,Promise
能夠幫助咱們解決這個問題!
首先,讓咱們重寫整個代碼塊,以便每一個函數返回一個 Promise
來代替以前的函數。
若是圖片被加載完成而且一切正常,讓咱們用加載完的圖片解決 (resolve)promise
。
不然,若是在加載文件時某個地方有一個錯誤,咱們將會用發生的錯誤拒絕 (reject)promise
。
讓咱們看下當咱們在終端運行這段代碼時會發生什麼?
很是酷!就像咱們所指望的同樣,promise
獲得瞭解析數據後的值。
可是如今呢?咱們不關心整個 promise
對象,咱們只關心數據的值!幸運的,有內置的方法來獲得 promise
的值。
對於一個 promise
,咱們可使用它上面的 3 個方法:
.then 方法接收傳遞給 resolve
方法的值。
.catch 方法接收傳遞給 rejected
方法的值。
最終,咱們擁有了 promise
被解決後 (resolved)
的值,並不須要整個 promise
對象!
如今咱們能夠用這個值作任何咱們想作的事。
順便提醒一下,當你知道一個 promise
老是 resolve
或者老是 reject
的時候,你能夠寫 Promise.resolve
或 Promise.reject
,傳入你想要 reject
或 resolve
的 promise
的值。
在下邊的例子中你將會常常看到這個語法。
在 getImage 的例子中,爲了運行它們,咱們最終不得不嵌套多個回調。幸運的,.then
處理器能夠幫助咱們完成這件事!
.then
它本身的執行結果是一個 promise
。這意味着咱們能夠連接任意數量的 .then
:前一個 then
回調的結果將會做爲參數傳遞給下一個 then
回調!
在 getImage 示例中,爲了傳遞被處理的圖片到下一個函數,咱們能夠連接多個 then
回調。
相比於以前最終獲得許多嵌套回調,如今咱們獲得了整潔的 then
鏈。
完美!這個語法看起來已經比以前的嵌套回調好多了。
咱們知道了一些如何建立 promise
以及如何提取出 promise
的值的方法。
讓咱們爲腳本添加一些更多的代碼而且再次運行它:
等下,發生了什麼?!
首先,Start!
被輸出。
好的,咱們已經看到了那一個即將到來的消息:console.log('Start!')
在最前一行輸出!
然而,第二個被打印的值是 End!
,並非 promise
被解決的值!只有在 End!
被打印以後,promise
的值纔會被打印。
這裏發生了什麼?
咱們最終看到了 promise
真正的力量!儘管 JavaScript
是單線程的,咱們可使用 Promise
添加異步任務!
等等,咱們以前沒見過這種狀況嗎?
在 JavaScript Event Loop 中,咱們不是也可使用瀏覽器原生的方法如 setTimeout
建立某類異步行爲嗎?
是的!然而,在事件循環內部,實際上有 2 種類型的隊列:宏任務(macro)隊列 (或者只是叫作 任務隊列 )和 微任務隊列。
(宏)任務隊列用於 宏任務,微任務隊列用於 微任務。
那麼什麼是宏任務,什麼是微任務呢?
儘管他們比我在這裏介紹的要多一些,可是最經常使用的已經被展現在下面的表格中!
(Macro)task: | setTimeout | setInterval | setImmediate |
Microtask: | process.nextTick | Promise callback | queueMicrotask |
咱們看到 Promise
在微任務列表中!當一個 Promise
解決 (resolve
) 而且調用它的 then()
、catch()
或 finally()
方法的時候,這些方法裏的回調函數被添加到微任務隊列!
這意味着 then(),chatch() 或 finally()
方法內的回調函數不是當即被執行,本質上是爲咱們的 JavaScript
代碼添加了一些異步行爲!
那麼何時執行 then(),catch(),或 finally() 內的回調呢?
事件循環給與任務不一樣的優先級:
當前在調用棧 (call stack) 內的全部函數會被執行。當它們返回值的時候,會被從棧內彈出。
當調用棧是空的時,全部排隊的微任務會一個接一個從微任務任務隊列中彈出進入調用棧中,而後在調用棧中被執行!(微任務本身也能返回一個新的微任務,有效地建立無限的微任務循環 )
若是調用棧和微任務隊列都是空的,事件循環會檢查宏任務隊列裏是否還有任務。若是宏任務中還有任務,會從宏任務隊列中彈出進入調用棧,被執行後會從調用棧中彈出!
讓咱們快速地看一個簡單的例子:
Task1: 當即被添加到調用棧中的函數,好比在咱們的代碼中當即調用它。
Task2,Task3,Task4: 微任務,好比 promise
中 then
方法裏的回調,或者用 queueMicrotask
添加的一個任務。
Task5,Task6: 宏任務,好比 setTimeout 或者 setImmediate
裏的回調
首先,Task1 返回一個值而且從調用棧中彈出。而後,JavaScript
引擎檢查微任務隊列中排隊的任務。一旦微任務中全部的任務被放入調用棧而且最終被彈出,JavaScript 引擎會檢查宏任務隊列中的任務,將他們彈入調用棧中而且在它們返回值的時候把它們彈出調用棧。
圖中足夠粉色的盒子是不一樣的任務,讓咱們用一些真實的代碼來使用它!
在這段代碼中,咱們有宏任務 setTimeout 和 微任務 promise 的 then 回調。
一旦 JavaScript 引擎到達 setTimeout 函數所在的那行就會涉及到事件循環。
讓咱們一步一步地運行這段代碼,看看會獲得什麼樣的日誌!
快速提一下:在下邊的例子中,我正在展現的像
console.log
,setTimeout
和Promise.resolve
等方法正在被添加到調用棧中。它們是內部的方法實際上沒有出如今堆棧痕跡中,所以若是你正在使用調試器,不用擔憂,你不會在任何地方見到它們。它只是在沒有添加一堆樣本文件代碼的狀況下使這個概念解釋起來更加簡單。
在第一行,JavaScript
引擎遇到了 console.log()
方法,它被添加到調用棧,以後它在控制檯輸出值 Start!。console.log
函數從調用棧內彈出,以後 JavaScript
引擎繼續執行代碼。
JavaScript
引擎遇到了 setTimeout
方法,他被彈入調用棧中。setTimeout
是瀏覽器的原生方法:它的回調函數 (() => console.log('In timeout'))
將會被添加到 Web API
,直到計時器完成計時。儘管咱們爲計時器提供的值是 0,在它被添加到宏任務隊列 (setTimeout
是一個宏任務) 以後回調仍是會被首先推入 Web API
。
JavaScript
引擎遇到了 Promise.resolve
方法。Promise.resolve
被添加到調用棧。在 Promise
解決 (resolve
) 值以後,它的 then
中的回調函數被添加到微任務隊列。
JavaScript
引擎看到調用棧如今是空的。因爲調用棧是空的,它將會去檢查在微任務隊列中是否有在排隊的任務!是的,有任務在排隊,promise
的 then
中的回調函數正在等待輪到它!它被彈入調用棧,以後它輸出了 promise
被解決後( resolved
)的值: 在這個例子中的字符串 Promise!
。
JavaScript
引擎看到調用棧是空的,所以,若是任務在排隊的話,它將會再次去檢查微任務隊列。此時,微任務隊列徹底是空的。
到了去檢查宏任務隊列的時候了:setTimeout
回調仍然在那裏等待!setTimeout
被彈入調用棧。回調函數返回 console.log
方法,輸出了字符串 In timeout!
。setTimeout
回調從調用棧中彈出。
終於,全部的事情完成了! 看起來咱們以前看到的輸出最終並非那麼出乎意料。
ES7
引入了一個新的在 JavaScript
中添加異步行爲的方式而且使 promise
用起來更加簡單!隨着 async
和 await
關鍵字的引入,咱們可以建立一個隱式的返回一個 promise
的 async
函數。可是,咱們該怎麼作呢?
以前,咱們看到不論是經過輸入 new Promise(() => {})
,Promise.resolve
或 Promise.reject
,咱們均可以顯式的使用 Promise
對象建立 promise
。
咱們如今可以建立隱式地返回一個對象的異步函數,而不是顯式地使用 Promise
對象!這意味着咱們再也不須要寫任何 Promise
對象了。
儘管 async
函數隱式的返回 promise
是一個很是棒的事實,可是在使用 await
關鍵字的時候才能看到 async
函數的真正力量。當咱們等待 await
後的值返回一個 resolved
的 promise
時,經過 await
關鍵字,咱們能夠暫停異步函數。若是咱們想要獲得這個 resolved
的 promise
的值,就像咱們以前用 then
回調那樣,咱們能夠爲被 await
的 promise
的值賦值爲變量!
這樣,咱們就能夠暫停一個異步函數嗎?很好,但這究竟是什麼意思?
當咱們運行下面的代碼塊時讓咱們看下發生了什麼:
額,這裏發生了什麼呢?
首先,JavaScript
引擎遇到了 console.log
。它被彈入到調用棧中,這以後 Before function!
被輸出。
而後,咱們調用了異步函數myFunc()
,這以後myFunc
函數體運行。函數主體內的最開始一行,咱們調用了另外一個console.log
,此次傳入的是字符串In function!
。console.log
被添加到調用棧中,輸出值,而後從棧內彈出。
函數體繼續執行,將咱們帶到第二行。最終,咱們看到一個await
關鍵字!
最早發生的事是被等待的值執行:在這個例子中是函數one
。它被彈入調用棧,而且最終返回一個解決狀態的promise
。一旦Promise
被解決而且one
返回一個值,JavaScript
遇到了await
關鍵字。
當遇到await
關鍵字的時候,異步函數被暫停。函數體的執行被暫停,async
函數中剩餘的代碼會在微任務中運行而不是一個常規任務!
如今,由於遇到了await
關鍵字,異步函數myFunc
被暫停,JavaScript
引擎跳出異步函數,而且在異步函數被調用的執行上下文中繼續執行代碼:在這個例子中是全局執行上下文!♀️
最終,沒有更多的任務在全局執行上下文中運行!事件循環檢查看看是否有任何的微任務在排隊:是的,有!在解決了one
的值之後,異步函數myFunc
開始排隊。myFunc
被彈入調用棧中,在它以前中斷的地方繼續運行。
變量res
最終得到了它的值,也就是one
返回的promise
被解決的值!咱們用res
的值(在這個例子中是字符串One!
)調用console.log
。One!
被打印到控制檯而且console.log
從調用棧彈出。
最終,全部的事情都完成了!你注意到async
函數相比於promise
的then
有什麼不一樣嗎?await
關鍵字暫停了async
函數,然而若是咱們使用then
的話,Promise
的主體將會繼續被執行!
嗯,這是至關多的信息!當使用Promise
的時候,若是你仍然感受有一點不知所措,徹底不用擔憂。我我的認爲,當使用異步JavaScript
的時候,只是須要經驗去注意模式以後便會感到自信。
當使用異步JavaScript
的時候,我但願你可能遇到的「沒法預料的」或「不可預測的」行爲如今變得更有意義!
外國友人技術博客的語言表達的方式和風格、與國人的仍是有很大差異的啊。
往往看到有很長或者很拗口的句子的時候,我就想按本身的語言來寫一篇了 ????
可能本身寫一篇都比翻譯的快 ????