本文適用環境爲 NodeJs v12 和 2019 年 11 月 19 日最新版 Chrome。promise
寫這篇文章的原由是在寫單元測試時,作形以下測試時bash
new Promise((resolve, reject) => reject(1)).then().catch(err => {
console.log(err)
})
async function jestTest () {
await Promise.resolve().then()
console.log('這個時候catch預期已經被調用,且輸出日誌')
}
jestTest()
複製代碼
沒法使用 await
將測試代碼剛好阻塞到 catch
在 Event Loop
中被調用後的時機,從而檢測到 catch
的執行,經過測試。異步
而使用「神奇」一詞則是由於 promsie 的鏈式調用中確實有不少默認的 handler 和值的隱含傳遞。async
爲了避免浪費你們的時間,咱們先看一個例子:ide
Promise.resolve('promise1')
.then(res => {
console.log('promise1-1 then')
})
.then(res => {
console.log('promise1-2 then')
})
.then(res => {
console.log('promise1-3 then')
})
.then(res => {
console.log('promise1-4 then')
})
Promise.resolve('promise2')
.then(res => {
console.log('promise2-1 then')
throw new Error('mock error 1')
})
.then(res => {
console.log('promise2-2 then')
throw new Error('mock error 2')
})
.catch(err => {
console.log(err)
})
複製代碼
若是你答出的上述代碼的輸出順序與下述相同,那麼你能夠跳過這篇文章:oop
promise1-1 then
promise2-1 then
promise1-2 then
promise1-3 then
Error: mock error 1
promise1-4 then
複製代碼
首先有一個前提,就是你已經知道了,這兩個 promise 的 then 的調用是交叉入棧的(從頭三行輸出也能看出來),若是不清楚這部份內容,能夠查閱 Event Loop 的相關文章,同時須要注意的是,在文章所指明的版本中 Chrome 與 NodeJs Event Loop 機制已經相同。單元測試
咱們去翻閱下 本來(我作了修改) MDN 關於 catch 的一段描述:測試
Basically, a promise chain stops if there's an exception, looking down the chain for catch handlers instead.ui
鏈式調用在發生異常時會中止,在鏈上查找 catch 語句來執行。spa
我最初的誤解與此相同,誤覺得 catch 會直接抓到第一個throw Error
,即 Error
會在 promise1-2
以後輸出,即 promise2-2
所在的 then
並不會被加入調用棧。
而經過觀察實際的輸出結果發現並不是如此,那麼能夠說明 MDN 解釋的字面意思應該是錯的,鏈式調用並無中止,而是執行了咱們沒看到的東西。
這時咱們須要知道 then
的一個默認處理,一樣直接引用 MDN 的描述:
If the Promise that then is called on adopts a state (fulfillment or rejection) for which then has no handler, a new Promise is created with no additional handlers, simply adopting the final state of the original Promise on which then was called.
若是你的 promise 的 then 缺乏了對應狀態處理的回調,那麼 then 會自動生成一個接受此 promise 狀態的 promise,即 then 會返回一個狀態引用相同的 promsie,交給後續的調用。
那麼上述代碼中的第二個 promise 部分就等效於
Promise.resolve('promise2')
.then(res => {
console.log('promise2-1 then')
throw new Error('mock error 1')
})
.then(res => {
console.log('promise2-2 then')
throw new Error('mock error 2')
// 注意這個 onRejected
}, (err) => {
return Promise.reject(err)
})
.catch(err => {
console.log(err)
})
複製代碼
也就是說在輸出結果的 promise1-2
和 promise1-3
之間是執行了 promise2-2
所在的 then
的,也就是說鏈式調用並無直接中止,promise2-2
所在的 then
仍是被加入了調用棧。而 catch
並非直接 catch
的第一個 then
拋出的錯誤,而是這個隱藏的 onRejected
返回的一樣狀態的 promise
。
同理咱們須要知道的是,catch(onRejected)
是 then(undefined, onRejected)
的簡寫,即就算調用鏈的前置調用沒有發生錯誤,catch
也是會進入調用棧而非直接跳過的。
Promise.resolve('promise1')
.then(res => {
console.log('promise1-1 then')
})
.then(res => {
console.log('promise1-2 then')
})
.then(res => {
console.log('promise1-3 then')
})
Promise.resolve('promise2')
.then(res => {
console.log('promise2-1 then')
})
.catch(err => {
console.log(err)
})
.then(res => {
console.log('其實我是 promise2-3 then')
})
複製代碼
首先須要注意的是在文章指明的 NodeJs 和 Chrome 版本中,f(await promise)
徹底等同於 promise.then(f)
。
固然,討論 promise
的時候,咱們也不能拋開 async await
。雖然二者在 promise 狀態爲 onResolve 時處理邏輯相同,但錯誤處理的執行邏輯並不同,在 async await
中發生錯誤時,纔是真正的直接跳事後續 await
的執行
const promiseReject = new Promise((resolve, reject) => {
reject(new Error('錯誤'))
})
const promiseResolve1 = new Promise((resolve, reject) => {
resolve('正確')
})
const promiseResolve2 = new Promise((resolve, reject) => {
resolve('正確')
})
const promiseResolve3 = new Promise((resolve, reject) => {
resolve('正確')
})
function demo1 () {
promiseReject
.then(() => {
console.log('1-1')
})
.catch(err => {
console.log('1-2')
})
}
async function demo2 () {
try {
await promiseReject
await promiseResolve1
await promiseResolve2
await promiseResolve3
} catch (error) {
console.log('2-1')
}
}
// 2-1
// 1-2
複製代碼
雖然這種執行時機幾乎沒有機會影響到實際的代碼,但仍是但願對各位的好奇心和異步代碼單元測試有所幫助。