JS異步代碼單元測試之神奇的Promise

前言

寫這篇文章的起因是在寫單元測試時,做形如下測試時

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 和值的隱含傳遞。

promise 的鏈式調用

為瞭不浪費大傢的時間,我們先看一個例子:

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)
})

如果你答出的上述代碼的輸出順序與下述相同,那麼你可以跳過這篇文章:

promise1-1 then

promise2-1 then

promise1-2 then

promise1-3 then

Error: mock error 1

promise1-4 then

首先有一個前提,就是你已經知道瞭,這兩個 promise 的 then 的調用是交叉入棧的(從頭三行輸出也能看出來),如果不清楚這部分內容,可以查閱 Event Loop 的相關文章,同時需要註意的是,在文章所指明的版本中 Chrome 與 NodejsEvent Loop 機制已經相同。

MDN 的錯誤

我們去翻閱下原本(我做瞭修改) MDN 關於 catch 的一段描述:

Basically, a promise chain stops if there’s an exception, looking down the chain for catch handlers instead.

鏈式調用在發生異常時會停止,在鏈上查找 catch 語句來執行。

我最初的誤解與此相同,誤以為 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')
})

async await

首先需要註意的是在文章指明的 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

以上就是JS異步代碼單元測試之神奇的Promise的詳細內容,更多關於JS異步代碼之Promise的資料請關註WalkonNet其它相關文章!

推薦閱讀: