關於 Promise 的執行順序

語言: CN / TW / HK

最近看到一個 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 會按照他們的呼叫順序直接放入微任務佇列中。