4.6. 什么是 Promise.prototype.done ?
如果你使用过其他的Promise实现类库的话,可能见过用done
代替then
的例子。
这些类库都提供了 Promise.prototype.done
方法,使用起来也和 then
一样,但是这个方法并不会返回promise对象。
虽然 ES6 Promises和Promises/A+等在设计上并没有对Promise.prototype.done
做出任何规定,但是很多实现类库都提供了该方法的实现。
在本小节中,我们将会学习什么是 Promise.prototype.done
,以及为什么很多类库都提供了对该方法的支持。
4.6.1. 使用done的代码示例
看一下实际使用done的代码的例子的话,应该就非常容易理解 done
方法的行为了。
promise-done-example.js
if (typeof Promise.prototype.done === 'undefined') {
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected).catch(function (error) {
setTimeout(function () {
throw error;
}, 0);
});
};
}
var promise = Promise.resolve();
promise.done(function () {
JSON.parse('this is not json'); // => SyntaxError: JSON.parse
});
// => 请打开浏览器的开发者工具中的控制台窗口看一下
在前面我们已经说过,promise设计规格并没有对 Promise.prototype.done
做出任何规定,因此在使用的时候,你可以使用已有类库提供的实现,也可以自己去实现。
我们会在后面讲述如何去自己实现,首先我们这里先对使用 then
和使用 done
这两种方式进行一下比较。
使用then的场景
var promise = Promise.resolve();
promise.then(function () {
JSON.parse("this is not json");
}).catch(function (error) {
console.error(error);// => "SyntaxError: JSON.parse"
});
从上面我们可以看出,两者之间有以下不同点。
done
并不返回promise对象- 也就是说,在done之后不能使用
catch
等方法组成方法链
- 也就是说,在done之后不能使用
done
中发生的异常会被直接抛给外面- 也就是说,不会进行Promise的错误处理(Error Handling)
由于done
不会返回promise对象,所以我们不难理解它只能出现在一个方法链的最后。
此外,我们已经介绍过了Promise具有强大的错误处理机制,而done
则会在函数中跳过错误处理,直接抛出异常。
为什么很多类库都提供了这个和Promise功能相矛盾的函数呢?看一下下面Promise处理失败的例子,也许我们多少就能理解其中原因了吧。
4.6.2. 消失的错误
Promise虽然具备了强大的错误处理机制,但是(调试工具不能顺利运行的时候)这个功能会导致人为错误(human error)更加复杂,这也是它的一个缺点。
也许你还记得,我们在 then or catch? 中也看到了类似的内容。
像下面那样,我们看一个能返回promise对象的函数。
json-promise.js
function JSONPromise(value) {
return new Promise(function (resolve) {
resolve(JSON.parse(value));
});
}
这个函数将接收到的参数传递给 JSON.parse
,并返回一个基于JSON.parse
的promise对象。
我们可以像下面那样使用这个Promise函数,由于 JSON.parse
会解析失败并抛出一个异常,该异常会被 catch
捕获。
function JSONPromise(value) {
return new Promise(function (resolve) {
resolve(JSON.parse(value));
});
}
// 运行示例
var string = "非合法json编码字符串";
JSONPromise(string).then(function (object) {
console.log(object);
}).catch(function(error){
// => JSON.parse抛出异常时
console.error(error);
});
如果这个解析失败的异常被正常捕获的话则没什么问题,但是如果编码时忘记了处理该异常,一旦出现异常,那么查找异常发生的源头将会变得非常棘手,这就是使用promise需要注意的一面。
忘记了使用catch进行异常处理的的例子
var string = "非合法json编码字符串";
JSONPromise(string).then(function (object) {
console.log(object);
}); (1)
1 | 虽然抛出了异常,但是没有对该异常进行处理 |
如果是JSON.parse
这样比较好找的例子还算好说,如果是拼写错误的话,那么发生了Syntax Error错误的话将会非常麻烦。
typo错误
var string = "{}";
JSONPromise(string).then(function (object) {
conosle.log(object);(1)
});
1 | 存在conosle这个拼写错误 |
这这个例子里,我们错把 console
拼成了 conosle
,因此会发生如下错误。
ReferenceError: conosle is not defined
但是,由于Promise的try-catch机制,这个问题可能会被内部消化掉。 如果在调用的时候每次都无遗漏的进行 catch
处理的话当然最好了,但是如果在实现的过程中出现了这个例子中的错误的话,那么进行错误排除的工作也会变得困难。
这种错误被内部消化的问题也被称为 unhandled rejection ,从字面上看就是在Rejected时没有找到相应处理的意思。
这种unhandled rejection错误到底有多难检查,也依赖于Promise的实现。 比如 ypromise 在检测到 unhandled rejection 错误的时候,会在控制台上提示相应的信息。
另外, Bluebird 在比较明显的人为错误,即ReferenceError等错误的时候,会直接显示到控制台上。
原生(Native)的 Promise实现为了应对同样问题,提供了GC-based unhandled rejection tracking功能。 该功能是在promise对象被垃圾回收器回收的时候,如果是unhandled rejection的话,则进行错误显示的一种机制。 |
4.6.3. done的实现
作为方法论,在Promise中 done
是怎么解决上面提到的错误被忽略呢? 其实它的方法很简单直接,那就是必须要进行错误处理。
由于可以在 Promise上实现 done
方法,因此我们看看如何对 Promise.prototype.done
这个Promise的prototype进行扩展。
promise-prototype-done.js
"use strict";
if (typeof Promise.prototype.done === "undefined") {
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected).catch(function (error) {
setTimeout(function () {
throw error;
}, 0);
});
};
}
那么它是如何将异常抛到Promise的外面的呢?其实这里我们利用的是在setTimeout中使用throw方法,直接将异常抛给了外部。
setTimeout的回调函数中抛出异常
try{
setTimeout(function callback() {
throw new Error("error");(1)
}, 0);
}catch(error){
console.error(error);
}
1 | 这个例外不会被捕获 |
关于为什么异步的 |
仔细看一下 Promise.prototype.done的代码,我们会发现这个函数什么也没 return
。 也就是说, done
按照「Promise chain在这里将会中断,如果出现了异常,直接抛到promise外面即可」的原则进行了处理。
如果实现和运行环境实现的比较完美的话,就可以进行 unhandled rejection 检测,done
也不一定是必须的了。 另外像本小节中的 Promise.prototype.done一样,done
也可以在既有的Promise之上进行实现,也可以说它没有进入到 ES6 Promises的设计规范之中。
本文中的 Promise.prototype.done 的实现方法参考了 promisejs.org 。 |
4.6.4. 总结
在本小节中,我们学习了 Q 、 Bluebird 和 prfun 等Promise类库提供的 done
的基础和实现细节,以及done
方法和 then
方法有什么区别等内容。
我们也学到了 done
有以下两个特点。
done
中出现的错误会被作为异常抛出终结 Promise chain
和 then or catch? 中说到的一样,由Promise内部消化掉的错误,随着调试工具或者类库的改进,大多数情况下也许已经不是特别大的问题了。
此外,由于 done
不会有返回值,因此不能在它之后进行方法链的创建,为了实现Promise方法风格上的统一,我们也可以使用done
方法。
ES6 Promises 本身提供的功能并不是特别多。 因此,我想很多时候可能需要我们自己进行扩展或者使用第三方类库。
我们好不容易将异步处理统一采用Promise进行统一处理,但是如果做过头了,也会将系统变得特别复杂,因此,保持风格的统一是Promise作为抽象对象非常重要的部分。
在 Promises: The Extension Problem (part 4) | getiblog 中,介绍了一些如何编写Promise扩展程序的方法。
此外,关于 Delegate 的详细使用方法,也可以参考 Chapter 28. Subclassing Built-ins ,那里有详细的说明。 |