全局的 Promise 拒绝处理
Promise 最有争议的方面之一就是:当一个 Promise 被拒绝时若缺少拒绝处理函数,就会静默失败。有人认为这是规范中最大的缺陷,因为这是 JS 语言所有组成部分中唯一不让错误清晰可见的。
由于 Promise 的本质,判断一个 Promise 的拒绝是否已被处理并不直观。例如,研究以下示例:
let rejected = Promise.reject(42);
// at this point, rejected is unhandled
// some time later...
rejected.catch(function(value) {
// now rejected has been handled
console.log(value);
});
无论 Promise 是否已被解决,你都可以在任何时候调用 then() 或 catch() 并使它们正确工作,这导致很难准确知道一个 Promise 何时会被处理。此例中的 Promise 被立刻拒绝,但它后来才被处理。
虽然下个版本的 ES 可能会处理此问题,不过浏览器与 Node.js 已经实施了变更来解决开发者的这个痛点。这些变更不是 ES6 规范的一部分,但却是使用 Promise 时的宝贵工具。
Node.js 的拒绝处理
在 Node.js 中,process 对象上存在两个关联到 Promise 的拒绝处理的事件:
-
unhandledRejection:当一个 Promise 被拒绝、而在事件循环的一个轮次中没有任何拒绝处理函数被调用,该事件就会被触发;
-
rejectionHandled:若一个 Promise 被拒绝、并在事件循环的一个轮次之后再有拒绝处理函数被调用,该事件就会被触发。
这两个事件旨在共同帮助识别已被拒绝但未曾被处理 promise。
unhandledRejection 事件处理函数接受的参数是拒绝原因(常常是一个错误对象)以及已被拒绝的 Promise。以下代码展示了 unhandledRejection 的应用:
let rejected;
process.on("unhandledRejection", function(reason, promise) {
console.log(reason.message); // "Explosion!"
console.log(rejected === promise); // true
});
rejected = Promise.reject(new Error("Explosion!"));
此例创建了一个带有错误对象的已被拒绝的 Promise,并监听了 unhandledRejection 事件。事件处理函数接收了该错误对象作为第一个参数, 原 Promise 则是第二个参数。
rejectionHandled 事件处理函数则只有一个参数,即已被拒绝的 Promise。例如:
let rejected;
process.on("rejectionHandled", function(promise) {
console.log(rejected === promise); // true
});
rejected = Promise.reject(new Error("Explosion!"));
// wait to add the rejection handler
setTimeout(function() {
rejected.catch(function(value) {
console.log(value.message); // "Explosion!"
});
}, 1000);
此处的 rejectionHandled 事件在拒绝处理函数最终被调用时触发。若在 rejected 被创建后直接将拒绝处理函数附加到它上面,那么此事件就不会被触发。因为立即附加的拒绝处理函数在 rejected 被创建的事件循环的同一个轮次内就会被调用,这样 rejectionHandled 就不会起作用。
为了正确追踪潜在的未被处理的拒绝,使用 rejectionHandled 与 unhandledRejection 事件就能保持包含这些 Promise 的一个列表,之后等待一段时间再检查此列表。例如:
let possiblyUnhandledRejections = new Map();
// when a rejection is unhandled, add it to the map
process.on("unhandledRejection", function(reason, promise) {
possiblyUnhandledRejections.set(promise, reason);
});
process.on("rejectionHandled", function(promise) {
possiblyUnhandledRejections.delete(promise);
});
setInterval(function() {
possiblyUnhandledRejections.forEach(function(reason, promise) {
console.log(reason.message ? reason.message : reason);
// do something to handle these rejections
handleRejection(promise, reason);
});
possiblyUnhandledRejections.clear();
}, 60000);
对于未处理的拒绝,这只是个简单追踪器。它使用了一个 Map 来储存 Promise 及其拒绝原因,每个 Promise 都是键,而它的拒绝原因就是相关的值。每当 unhandledRejection 被触发,Promise 及其拒绝原因就会被添加到此 Map 中。而每当 rejectionHandled 被触发,已被处理的 Promise 就会从这个 Map 中被移除。这样一来,possiblyUnhandledRejections 就会随着事件的调用而扩展或收缩。setInterval() 的调用会定期检查这个列表,查看可能未被处理的拒绝,并将其信息输出到控制台(在现实情况下,你可能会想做点别的事情, 以便记录或处理该拒绝)。此例使用了一个 Map 而不是 Weak Map,这是因为你需要定期检查此 Map 来查看哪些 Promise 存在,而这是使用 Weak Map 所无法做到的。
尽管此例仅针对 Node.js,但浏览器也实现了类似的机制来将未处理的拒绝通知给开发者。
浏览器的拒绝处理
浏览器同样能触发两个事件,来帮助识别未处理的拒绝。这两个事件会被 window 对象触发,并完全等效于 Node.js 的相关事件:
-
unhandledrejection:当一个 Promise 被拒绝、而在事件循环的一个轮次中没有任何拒绝处理函数被调用,该事件就会被触发;
-
rejectionHandled:若一个 Promise 被拒绝、并在事件循环的一个轮次之后再有拒绝处理函数被调用,该事件就会被触发。
Node.js 的实现会传递分离的参数给事件处理函数,而浏览器事件的处理函数则只会接收到包含下列属性的一个对象:
-
type:事件的名称("unhandledrejection" 或 "rejectionhandled") ;
-
promise:被拒绝的 Promise 对象;
-
reason:Promise 中的拒绝值(拒绝原因) 。
浏览器的实现中存在的另一个差异就是:拒绝值(reason)在两种事件中都可用。 例如:
let rejected;
window.onunhandledrejection = function(event) {
console.log(event.type); // "unhandledrejection"
console.log(event.reason.message); // "Explosion!"
console.log(rejected === event.promise); // true
};
window.onrejectionhandled = function(event) {
console.log(event.type); // "rejectionhandled"
console.log(event.reason.message); // "Explosion!"
console.log(rejected === event.promise); // true
};
rejected = Promise.reject(new Error("Explosion!"));
此代码使用了 DOM 0 级写法的 onunhandledrejection 与 onrejectionhandled,对两个事件处理函数都进行了赋值(若你喜欢,也可以使用 addEventListener("unhandledrejection") 与 addEventListener("rejectionhandled"))。每个事件处理函数都接收一个事件对象,其中包含与被拒绝的 Promise 有关的信息,type、promise 与 reason 属性都可用。
以下代码在浏览器中追踪未被处理的拒绝,与 Node.js 的代码非常相似:
let possiblyUnhandledRejections = new Map();
// when a rejection is unhandled, add it to the map
window.onunhandledrejection = function(event) {
possiblyUnhandledRejections.set(event.promise, event.reason);
};
window.onrejectionhandled = function(event) {
possiblyUnhandledRejections.delete(event.promise);
};
setInterval(function() {
possiblyUnhandledRejections.forEach(function(reason, promise) {
console.log(reason.message ? reason.message : reason);
// do something to handle these rejections
handleRejection(promise, reason);
});
possiblyUnhandledRejections.clear();
}, 60000);
这个实现与 Node.js 的实现几乎一模一样。使用了相同方法在 Map 中存储 Promise 及其拒绝值,并在此后进行检查。唯一真正的区别就是在事件处理函数中信息是从何处被提取出来的。
处理 Promise 的拒绝可能很麻烦,但你才刚开始见识 Promise 实际上到底有多强大。现在是时候更进一步了——把几个 promises 串联在一起使用。