關於 Promise 的執行順序

Shenfq發表於2022-01-21

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

相關文章