神奇的 Promise —— 一次异步代码的单元测试
本文适用环境为 NodeJs v12 和 2019 年 11 月 19 日最新版 Chrome。
写这篇文章的起因是在写单元测试时,做形如下测试时
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 与 NodeJs Event 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
结尾
虽然这种执行时机几乎没有机会影响到实际的代码,但还是希望对各位的好奇心和异步代码单元测试有所帮助。