1.背景
最初,是想处理在 V8 中正确处理 promise unhandledRejection
事实上,基于 V8 的 Chrome 和 Node 都提供了方法监听 unhandledRejection
// 浏览器
window.addEventListener("unhandledrejection", event => {
console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});
// Node
process.on('unhandledRejection', (reason, promise) => {
console.log('promise:', promise, 'reason:', reason);
});
最初我以为这是基于 V8 的简单封装,类似给 V8 加一个 listener,然后再把 unhandledRejection event 暴露给业务层,但是后来发现并不是,其中的过程比我想象的复杂。
2.初探
由于 V8 的文档很少,我基本上就是基于 Node 的源码结合搜索来找寻答案。最初,在 Isolate (V8 的一个类,需要了解可以自行 Google ) 中找到了一个 SetPromiseRejectCallback 的方法,我以为大功告成,于是开始编写代码
//js 测试代码
new Promise((resolve, reject)=>{
throw 'unhandled rejection'
})
//c++ 处理代码
...
isolate->SetPromiseRejectCallback(PromiseRejectCallback)
...
void PromiseRejectCallbackJ2V8(PromiseRejectMessage message){
if (event == kPromiseRejectWithNoHandler) {
...
} else if (event == kPromiseHandlerAddedAfterReject) {
...
}
}
编译,运行,发现确实可以收到 'kPromiseRejectWithNoHandler' 事件,很欣慰。
基于此,我又改了一下测试代码,如下
//js 测试代码
new Promise((resolve, reject)=>{
throw 'unhandled rejection'
}).catch(error => {
})
再 run 一次,发现也能收到 'kPromiseRejectWithNoHandler' 事件,不过同时也会收到 'kPromiseHandlerAddedAfterReject' 事件,感觉事情隐隐约约起了变化。
基于 'kPromiseRejectWithNoHandler' 和 'kPromiseHandlerAddedAfterReject' 二者名字的简单推断
- 当一个 Promise 变为 reject 状态的时候,会检查有没有 handler,如果没有,触发 'kPromiseRejectWithNoHandler'
- 在 Promise 变为 reject 状态之后,再添加 handler,会触发 'kPromiseRejectWithNoHandler'
所以这边收到了两次事件。不过出于正常思维,显然,第二次的测试代码,不应该触发 'unhandledrejection', 那这个地方我们怎么过滤呢?
3. 方案
Node 源码中搜索 'SetPromiseRejectCallback',很 easy 的让我们找到了 Node 的实现机制。
这个找寻的过程可以看 @Starkwang 的大作,就不重复了
Starkwang:Node.js内部是如何捕获异步错误的?zhuanlan.zhihu.com简单归纳:
- Node 会监听每次 'kPromiseRejectWithNoHandler' 并把 promise 放入一个数组 pendingUnhandledRejections 内
- 收到 'kPromiseRejectWithNoHandler' 事件后,会把这个 promise 从 pendingUnhandledRejections 删除
- 在每次 Ticks 执行完毕后,会检查 pendingUnhandledRejections 有没有剩余的 promise,如果有的话,触发 'unhandledRejection'
这里可能有一些不太好理解,为什么要每次 Ticks 执行完毕后去检查。
4.原理
首先,我们需要熟悉事件循环
Node.js 事件循环,定时器和 process.nextTick() | Node.jsnodejs.org在事件循环中,process.nextTick()
从技术上讲不是事件循环的一部分,每次执行完一个事件(有些地方会称之为 宏任务),Ticks 会被执行。
在我们的场景中,也可以理解为 每次执行完一个事件(有些地方会称之为 宏任务),对于 'pendingUnhandledRejections' 的检查会发生。
也就是说,这里 Node 打了一个时间差。
如果一个 'kPromiseRejectWithNoHandler' 和 'kPromiseHandlerAddedAfterReject' 在同一个事件周期内触发,那么 'unhandledRejection' 会被触发,否则,不触发(Node 上会触发一个 'rejectionHandled' 事件)
好了,原理就是这样,不过这里有一个问题 Tick 是 Node 自己的一个概念,如果我们自己基于 V8 写代码的话,自然无法这么写。
我们的最初目的是想对一个事件周期触发的 'kPromiseRejectWithNoHandler' 和 'kPromiseHandlerAddedAfterReject' promise 进行去除,所以出于这个目的,只需要在像 Node 一样,在事件周期之间插入一个方法来检查。
最初我使用的方法是在每次接收到 'kPromiseRejectWithNoHandler' 事件的时候,构造一个 promise,在 promise 的回调函数去去检查。
为什么这么做呢?因为 promise 的回调函数是在微任务中执行的,而微任务就是在 事件周期之间 清空的,于是用这种比较 trick 的方法实现了。
//c++ 处理代码
...
isolate->SetPromiseRejectCallback(PromiseRejectCallback)
...
void PromiseRejectCallbackJ2V8(PromiseRejectMessage message){
// 调用 js 的 promiseRejectHandler 方法
...
}
//js 处理代码
function promiseRejectHandler(type, promise, reason) {
switch (type) {
case 'kPromiseRejectWithNoHandler':
unhandledRejection(promise, reason);
break;
...
}
}
function unhandledRejection(promise, reason) {
...
// 在 promise 的回调函数去去检查
Promise.resolve(1).then(value=>{
processPromiseRejections();
})
}
function processPromiseRejections(){
// 检查
}
不过,其实 V8 提供了直接插入微任务的方式 'EnqueueMicrotask',所以其实我们也可以去掉这种 trick 的方式