1. 程式人生 > 程式設計 >JS非同步程式碼單元測試之神奇的Promise

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('promihttp://www.cppcns.comse1-1 then')
})
.then(res www.cppcns.com=> {
    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')
.thehttp://www.cppcns.comn(res => {
    console.log('promise2-1 then')
    throw new Error('mock error 1')
})
.then(res => {
    rVPpDrLconsole.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(rVPpDrL'2-1')
    }
}
// 2-1
// 1-2

以上就是JS非同步程式碼單元測試之神奇的Promise的詳細內容,更多關於JS非同步程式碼之Promise的資料請關注我們其它相關文章!