最近看到一個 Promise 相關的很有意思的程式碼:
new Promise((resolve) => {
console.log(1)
resolve()
}).then(() => {
new Promise((resolve) => {
console.log(2)
resolve()
}).then(() => {
console.log(4)
})
}).then(() => {
console.log(3)
})
第一次看到這個程式碼的時候,以為的輸出結果會是:1,2,3,4
,但是被實際的輸出結果打臉 。
如圖所示,實際的輸出結果為:1,2,4,3
。
程式碼分析
為了搞清楚實際的輸出結果為什麼是:1,2,4,3
,我們來一步步分析程式碼的執行。
我們知道,Promise 例項化時,傳入的回撥會立即執行,而Promise 的 then 回撥會被放到微任務佇列中,等待執行。佇列就是一個先進先出的列表,先被放到佇列的回撥,會被優先執行。前面的程式碼中,一共有 5 個回撥函式。
回撥1
是 Promise 例項化時的回撥,所以會立即執行,此時控制檯列印出數字 1
,然後 resolve()
方法被呼叫,此時的 Promise 狀態被修改成了 fulfilled
(如果沒有呼叫 resolve()
方法,Promise 的狀態為 pending
)。
Promise 例項化完成後,第一個 then()
方法被呼叫, 回撥2
會被放入了微任務佇列中,等待執行。
then 方法何時呼叫?
這個時候疑問點來了,第一個 then()
方法被呼叫後,第二個 then
方法會不會馬上被呼叫,如果會,那輸出的結果就應該是 :1,2,3,4
。顯然,此時不會馬上呼叫第二個 then()
方法,也就是不會馬上將 回撥5
放入微任務佇列。那如果不會,那何時才會被呼叫?
這個時候,需要看一下 Promise/A+ 規範。重點是下面幾條:
2.2
then
方法
promise 的 then 方法接受兩個引數:promise.then(onFulfilled, onRejected)
2.2.2 如果 onFulfilled 是函式:
- 2.2.2.1 當 promise 處於已處理狀態時,該函式必須被呼叫並將 promise 的值作為第一個引數。
- 2.2.2.2 該函式一定不能在 promise 處於已處理狀態之前呼叫。
- 2.2.2.3 該函式被呼叫次數不超過一次。
2.2.6
then
可以在同一個 promise 上多次呼叫。
- 2.2.6.1 如果
promise
處於已處理狀態時,所有相應的onFulfilled
回撥必須按照它們對then
的組織順序依次呼叫。- 2.2.6.2 如果
promise
處於已拒絕狀態時,所有相應的onRejected
回撥必須按照它們對then
的組織順序依次呼叫。2.2.7
then
必須返回一個 promise。
promise1 = new Promise(resolve => resolve())
// promise1 可以多次呼叫 then
// 且 onFulfilled 回撥的執行順序,按照 .then 的呼叫順序執行
promise1.then(onFulfilled1) // 1
promise1.then(onFulfilled2) // 2
promise1.then(onFulfilled3) // 3
// 上面 3 個 onFulfilled,按照 1、2、3 的順序執行
// 呼叫 .then 方法後,返回一個新的 promise
promise2 = promise1.then(onFulfilled, onRejected);
綜上,第一個 then()
方法呼叫後,會返回一個新的 Promise。這樣做的目的就是為了保持鏈式呼叫,而且 then()
方法內的 onFulfilled
回撥會等待 Promise 狀態修改之後才會呼叫。
我們稍微修改一下前面程式碼的呼叫形式,如下:
const p1 = new Promise((resolve) => {
console.log(1)
resolve()
})
const p2 = p1.then(() => {
new Promise((resolve) => {
console.log(2)
resolve()
}).then(() => {
console.log(4)
})
})
const p3 = p2.then(() => {
console.log(3)
})
p1.then()
會返回一個新的 Promise 命名為 p2
,後面的 p2.then()
的回撥會在 p1.then()
內的回撥函式執行完之後,才會呼叫,也就是 p2
這個 Promise 狀態發生改變之後。
所以,只有 回撥2
執行完成後,才會執行 p2.then()
。我們再看 回撥2
的內容。
回撥2
先是對一個 Promise 進行了例項化操作,例項化的回撥為 回撥3
,該回撥會立即執行,此時控制檯列印出數字 2
,然後 resolve()
方法被呼叫,此時的 Promise 狀態被修改成了 fulfilled
,後面的 回撥4
會放入微任務佇列。回撥2
執行完畢後,執行 p2.then()
,回撥5
被放入微任務佇列。
按照佇列先進先出的執行順序,先執行 回撥4
,然後執行 回撥5
。所以,在控制檯會先輸出數字 4
,然後輸出數字 3
。
如果想要輸出的結果為:1,2,3,4
,可以將程式碼改成如下形式:
const p1 = new Promise((resolve) => {
console.log(1)
resolve()
})
p1.then(() => {
new Promise((resolve) => {
console.log(2)
resolve()
}).then(() => {
console.log(4)
})
})
p1.then(() => {
console.log(3)
})
根據前面的 2.2.6
規則,then
可以在同一個 promise 上多次呼叫,且 p1 後面的 then 會按照他們的呼叫順序直接放入微任務佇列中。