上次咱們從高階函數聊到了 promise
,此次咱們聊聊:javascript
promise A+
規範和 promise
應用來看 promise
的特性promise
和 eventloop 的關係薛定諤的貓是奧地利著名物理學家薛定諤提出的一個思想實驗,那麼這和 promise
有什麼關係呢?在這個著名的實驗中,假設在盒子裏會有一隻貓,而後咱們打開盒子只會出現兩個結果,貓死了或者是活着:html
那麼 promise
也相似,根據 promise A+ 規範 當一個 promise
被建立出來之後,它就擁有三種可能狀態 Pending (初始時爲 pending)/ Fulfilled / Rejected 若是咱們把範圍放寬一點,那麼 Fulfilled / Rejected 又能夠被稱爲 Settled:前端
okay,相信你已經理解了 promise
的三種狀態,那細心同窗看到上面有 then()
和 catch()
這樣的方法可能不理解,咱們再回到上面貓的例子裏面,如今這個科學家比較變態,在第一次實驗以後,貓出現了兩種狀態,可是他並沒結束實驗,而是針對這兩種狀況作了處理並繼續了實驗:html5
與之相似,一個完整的 promise
,在 Pending 狀態發生變化時,只多是兩種狀況,Fulfilled 和 Rejected,而且咱們能夠看到箭頭是單向的,意味着這個過程是 不可逆 的。java
這意味着,當 Pending
狀態發生了變化,不管是變成 Fulfilled
仍是 Rejected
都沒法再改變了。node
針對這兩種狀況,咱們在 then()
裏面能夠傳入兩個回調函數 onFulfillment
和 onRejection
做爲來處理不一樣的狀況。git
從圖中咱們能夠看到,當 onFulfillment
時,咱們一般會作一些異步的操做,而 onRejection
一般是作錯誤處理。而後咱們把當前的 promise
從新返回,直到下次他的 then()
再次被執行。github
一個promise.then().then().then()
這樣的方式就是咱們 上一篇文章 中所說的 鏈式調用。web
經過上一節,咱們已知 promise
自己的幾個特性:vim
promise
有三種狀態: Pending (初始時爲 pending)/ Fulfilled / Rejected。promise
狀態的轉變是不可逆的: Pending -> Fulfilled 或者 Pending -> Rejected 。promise
支持 then()
的鏈式調用。可是還有一些特性,咱們須要從代碼的角度來分析。
由於 promise
原意爲承諾,也就是我預先承諾了未來要達成的一件事情。
因此有同窗會認爲必須等到承諾兌現,也就是 promise
的狀態從 Pending
變爲 Fulfilled
或者 Rejected
時,其構造函數接收的函數纔會被執行。
可是實際上,一個 promise
被建立時,即便咱們沒有定義 then()
,其構造函數接收的函數也會當即執行:
let p = new Promise((resolve, reject) => {
console.log('A new promise was created1')
console.log('A new promise was created2')
console.log('A new promise was created3')
setTimeout(() => {
console.log('log setTimeout')
}, 3000)
resolve('success')
})
console.log('log outside')
複製代碼
輸出結果:
A new promise was created1
A new promise was created2
A new promise was created3
log outside
log setTimeout
複製代碼
根據 promise A+ 規範 , promise
的 then()
接收2個參數:
promise.then(onFulfilled, onRejected)
複製代碼
其中 onFulfilled
執行結束後調用,onRejected
拒絕執行後調用,看看這段代碼:
let p = new Promise((resolve, reject) => {
reject('reject')
//throw 'error'
})
p.then(
data => {
console.log('1:', data)
},
reason => {
console.log('reason:', reason)
}
)
複製代碼
最後打印的是:
reason: reject
複製代碼
能夠正常運行不是嗎?可是咱們發現實際應用中,咱們並無這樣來定義 then()
:
p.then(
data => {
console.log('1:', data)
},
reason => {
console.log('reason1:', reason)
}
).then(
data => {
console.log('2:', data)
},
reason => {
console.log('reason2:', reason)
}
).then(
data => {
console.log('3:', data)
},
reason => {
console.log('reason3:', reason)
}
)
複製代碼
而是使用 catch()
配合 onFulfilled()
:
p.then(data => {
console.log('1:', data)
}).then(data => {
console.log('2:', data)
}).then(data => {
console.log('3:', data)
}).catch(e => {
console.log('e2:', e)
})
複製代碼
表面上看,達到的效果是同樣的,因此這樣有什麼好處呢?
onFulfilled()
中若是發生錯誤,也會進行捕獲,不會中斷代碼的執行。看一段代碼:
let p = new Promise((resolve, reject) => {
console.log('A new promise was created1')
console.log('A new promise was created2')
console.log('A new promise was created3')
resolve('success')
})
console.log('log outside')
p.then(data => {
console.log('then:', data)
})
複製代碼
執行結果:
A new promise was created1
A new promise was created2
A new promise was created3
log outside
then: success
複製代碼
咱們能夠很清楚的看到,then()
中打印的內容是在最後的,爲何會這樣呢?由於 p.then()
中傳入的函數會被推入到 microtasks
(異步任務隊列的一種) 中,而任務隊列都是在執行棧中的代碼(同步任務)以後處理。
下面這些代碼都在同步任務中處理:
console.log('A new promise was created1')
console.log('A new promise was created2')
console.log('A new promise was created3')
console.log('log outside')
複製代碼
okay 看到這裏你可能會有一些問題,例如:
要明白這些,就不得不聊聊 Event loop。
在 W3C文檔 中咱們能夠找到關於它的描述:
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.
翻譯一下就是:
客戶端必須使用本章節中所描述的事件循環,來協調事件,用戶交互,腳本,呈現,網絡等等。 事件循環有兩種:用於瀏覽上下文的事件循環和用於 worker 的事件循環。
咱們寫好一段 JavaScript 代碼,而後瀏覽器打開這個頁面,或者在 node
環境中運行它,就能夠獲得咱們指望的結果,可是這段代碼怎麼執行的呢?
不少同窗都知道,是 JavaScript
引擎在執行代碼,而 JavaScript
引擎都是依託於一個宿主環境的,最通用的 JavaScript
宿主環境是瀏覽器。
這和 EventLoop 有什麼關係呢?
由於宿主環境是瀏覽器,因此 JavaScript
引擎被設計爲單線程。
爲何不能是多線程呢?舉個例子:加入咱們同時兩個線程都操做同一個 DOM
元素,那應該如何處理呢?對吧。
okay,既然是單線程,意味着咱們只能順序執行代碼,可是若是咱們執行某一行特別耗費時間,是否是在這行後面的內容就被阻塞了呢?
因此咱們須要在單線程的引擎中來實現異步,而 Event loop 就是實現異步的關鍵。
首先當一段代碼給到 JavaScript 引擎的時候,會區分這段代碼是同步仍是異步:
異步的代碼加入到任務隊列中,而任務隊列又分爲 宏任務隊列(macro tasks) 和 微任務隊列(micro tasks)。
一個瀏覽器的上下文環境可能對應有多個宏任務隊列可是隻有一個微任務隊列。你可能以爲會是這樣:
可是實際上,每一個宏任務都包含了一個微任務隊列:
那麼問題來了,咱們怎麼去判斷這段代碼要加入到宏任務隊列,仍是微任務隊列中呢?
咱們參考下文檔 中的解讀:
Each task is defined as coming from a specific task source. All the tasks from one particular task source and destined to a particular event loop
每一個任務都由特殊任務源來定義。 來自同一個特殊任務源的全部任務都將發往特定事件循環
因此咱們能夠按照不一樣的來源進行分類,不一樣來源的任務都對應到不一樣的任務隊列中
I/O
, setTimeout + setInterval + setImmediate
, UI renderder
···Promise
,process.nextTick
,MutationObserver
, Object.observe
···明白了這些概念以後,咱們來看看完整的執行過程。
下圖參考了 Philip Roberts的演講 PPT同時加深和細化:
圖的順序從上往下看:
JavaScript
引擎對全部的代碼進行區分。步驟 3 - 4 - 5
就是一個事件循環的基本原理。
不知道這篇文章有沒有讓你充分理解呢?有任何想法和建議,都留下你的評論吧~
小冊 你不知道的 Chrome 調試技巧 已經開始預售啦。
歡迎關注公衆號 「前端惡霸」,掃碼關注,好貨等着你~