神奇的 Promise —— 一次异步代码的单元测试

本文适用环境为 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 将测试代码刚好阻塞到 catchEvent Loop 中被调用后的时机,从而检测到 catch 的执行,经过测试。异步

而使用“神奇”一词则是由于 promsie 的链式调用中确实有不少默认的 handler 和值的隐含传递。async

promise 的链式调用

为了避免浪费你们的时间,咱们先看一个例子: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 的错误

咱们去翻阅下 本来(我作了修改) 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-2promise1-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
复制代码

结尾

虽然这种执行时机几乎没有机会影响到实际的代码,但仍是但愿对各位的好奇心和异步代码单元测试有所帮助。

相关文章
相关标签/搜索