本文为翻译,原文链接如下:What is Promise.try, and why does it matter?
引言
在 Node.js 社区中一个经常混淆用户的主题是由 Bluebird 提供的 Promise.try 方法。人们总是很难理解它是什么,或者为什么要使用它,而且几乎所有的 Promises 指南都没有很好地说明它的用途。
在这篇简短的文章中,我希望能更好地解释 Promise.try 是什么,以及为什么你应该经常使用它。我假设你已经对 Promise 有了一些了解,特别是.then 方法的用途。
即使你使用 Promises 的其它实现形式(例如 ES6 Promises),本文仍然对你有用。因为在本文的最后,我将解释如何在没有 Bluebird 的情况下实现相同的功能。
Promise.try 是什么
简而言之,Promise.try 就像 promiseObject.then,但是不需要前面的 promise 对象。现在这仍然有点模糊,所以让我们从一个例子开始。
一些使用 Promises 的典型代码可能如下所示:
function getUsername(userId) {
return database.users.get({id: userId})
.then(function(user) {
return user.name;
});
}
到现在为止还挺好。我们假设 database.users.get 将返回某种类型的 Promise,并且这个 Promise 最终将 resolve 一个具有 name 属性的对象。
下面是使用 Promise.try 的功能相同的代码:
var Promise = require(“bluebird”);
function getUsername(userId) {
return Promise.try(function() {
return database.users.get({id: userID});
}).then(function(user) {
return user.name;
});
}
如你所见,我们在调用链的最前端添加了 Promise.try。不同于直接在 database.users.get 后面链式调用,我们在 Promise.try 后面链式调用,并简单地返回了 database.users.get 的结果,就像我们通常用.then 做的那样。
Promise.try 有什么用
以上可能看起来像不必要的额外代码。但在实践中,它有几个优点:
更好的错误处理:无论同步错误在何处发生,它们会作为拒绝传递下去;
更好的互操作性:无论第三方方法使用什么样的 Promises 实现形式,你都将始终使用你首选的实现形式;
易于浏览:所有代码都水平地处于相同的缩进级别,因此更容易一目了然地看到正在发生的事情。
下面,我将分别讨论这些要点。
1、更好的错误处理
Promises 的一个受欢迎的优点是,我们可以用同样的方式处理同步错误和异步错误 —— 简单地捕获同步错误,并用一个拒绝的 Promise 将它传递下去。但情况确实如此吗?让我们看一下第一个例子略微修改之后的版本:
function getUsername(userId) {
return database.users.get({id: userId})
.then(function(user) {
return uesr.name;
});
}
在这个修改过的版本中,我们错误地输入了 user.name,现在它是 uesr.name。这通常会失败,因为 uesr 是未定义的,因此不能具有任何属性。事实上,正如我们想的那样,它将在.then 方法中被捕获,并变成一个拒绝的 Promise。
但是如果 database.users.get 同步抛出怎么办?如果第三方数据库代码中存在拼写错误或其他什么错误怎么办?Promises 通过将所有同步代码放入.then 方法中来实现错误捕获功能,因此它可以将这些代码包装在一个巨大的 try/catch 块中(译者注:promiseObj.then 方法的回调函数是在 try/catch 块中调用的,因此可以捕获错误)。
但是……我们的 database.users.get 不在.then 中!因此,Promises 的实现机制无法访问该代码块,也无法将其包装起来。我们的同步错误将仍是同步错误,现在我们又回到了“原始时代”—— 不得不分别处理同步和异步两种错误。
现在,让我们再回顾一下使用 Promise.try 的示例:
var Promise = require(“bluebird”);
function getUsername(userId) {
return Promise.try(function() {
return database.users.get({id: userID});
}).then(function(user) {
return user.name;
});
}
之前我说 Promise.try 就像一个.then,但不需要前面的 Promise。这也适用于此 —— 它将捕获 database.users.get 中的同步错误,就像.then 一样!(译者注:try 同步地捕获同步错误,then 异步地捕获同步错误,then 生生地将同步错误变成了 ’ 异步 ’ 错误)
通过使用 Promise.try,我们将我们的错误处理简化成涵盖了所有的同步错误的错误处理,而不仅仅是那些在第一次异步操作之后的错误处理(正如.then 的回调函数)。
Promises/A+
在我们继续讨论下一点之前,让我们来看看 Promises / A+ 是什么,以及它在生态系统中扮演的角色。Promises / A+ 官网如下总结它:
实现者为实现者提供的一个可靠的,可互操作的开放的 JavaScript Promise 标准。
换句话说,这是一种确保 Promise 的不同实现形式(Bluebird,ES6,Q,RSVP,……)完美协同工作的方法。这个 Promises / A+ 规范就是为什么你可以使用任何你喜欢的 Promises 实现形式,以及为什么你不必关心第三方库(比如 Knex)使用哪种 Promises 实现形式的原因。
为了说明 Promises / A+ 为用户做了什么:所有用红色高亮的函数返回 Bluebird Promises,蓝色的那个返回 ES6 Promise,绿色的那个返回 Q Promise。
请注意,即使我们在第一个.then 的回调中返回了 ES6 Promise,第一个.then 仍然会返回 Bluebird Promise。类似地,即使第二个.then 的回调返回 Q Promise,我们的 doStuff 方法仍将返回 Bluebird Promise。
发生这种情况是因为,除了捕获同步错误之外,.then 还会包装返回值,并确保它们最终成为与调用.then 方法的 promise 对象的实现形式相同的 promise 对象。实际上,这意味着调用链中的第一个 promise 对象决定了你之后将使用的实现形式。
这是一种确保你始终使用可预测的 API 的实用方法。你必须关注的唯一一个 Promise 实现形式,是链中的第一个函数使用什么形式 —— 无论后来发生什么。
2、更好的互操作性
然而,上述规范并不总是令人满意的 —— 例如,看看这个例子:
var Promise = require(“bluebird”);
function getAllUsernames() {
// ⬐ This will return an ES6 Promise.
return database.users.getAll()
.map(function(user) {
return user.name;
});
}
如果您不熟悉 map 并且想了解更多信息,可以阅读本文。
我们在此特定示例中使用的.map 功能是一个 Bluebird 功能,并且在 ES6 Promises 上不可用。我们不能在这里使用它,即使我们在项目中使用 Bluebird —— 这是因为链中的第一个函数(database.users.getAll)返回了一个 ES6 Promise 而不是 Bluebird Promise。
现在,让我们再看一个相同的例子,但这次使用 Promise.try:
var Promise = require(“bluebird”);
function getAllUsernames() {
// ⬐ This will return a Bluebird Promise.
return Promise.try(function() {
// ⬐ This will return an ES6 Promise.
return database.users.getAll();
}).map(function(user) {
return user.name;
});
}
现在我们可以使用.map 了!因为我们是从源自 Bluebird 的 Promise.try 开始的,之后我们所有的 Promise 也将是 Bluebird Promises,无论.then 的回调函数中发生了什么。
通过像这样使用 Promise.try,你可以通过确保链中的第一个 Promise 来自你的首选实现形式,来决定自己要使用哪种实现形式。你无法用.then 做到这件事,因为链首并不总是你想要的那种实现形式。
3、易于浏览
最后的优点是可读性。通过使用 Promise.try 启动每个链,所有实际的“业务逻辑”都存在于相同(水平)缩进级别的回调中。虽然这似乎是一个小的优点,但在人类浏览大量文本的时候,它实际上会产生相当大的差异。
为了说明差异,这是在没有 Promise.try 而使用常见缩进样式的情况下直观浏览代码的方法:而这是使用 Promise.try 的同样代码:尽管代码看起来有些“吵闹”,但仍然可以更容易地快速了解代码,只是因为你的眼睛可以“寻找”更少。
怎么实现 Promise.try
对于那些未支持 Promise.try 的 Promise 实现形式,只要它们的 Promise 构造函数可以捕获同步错误,就可以很容易地实现 Promise.try 功能。ES6 Promise 的构造函数就能够捕获同步错误,所以我们可以这样实现 Promise.try:
‘use strict’;
export function promiseTry(func) {
return new Promise(function(resolve, reject) {
resolve(func());
});
}
目前我的 polyfill 也新增了 Promise.try 方法:https://github.com/lyl123321/…