4

本文为翻译,原文链接如下:
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 有什么用

以上可能看起来像不必要的额外代码。但在实践中,它有几个优点:

  1. 更好的错误处理:无论同步错误在何处发生,它们会作为拒绝传递下去;
  2. 更好的互操作性:无论第三方方法使用什么样的 Promises 实现形式,你都将始终使用你首选的实现形式;
  3. 易于浏览:所有代码都水平地处于相同的缩进级别,因此更容易一目了然地看到正在发生的事情。

下面,我将分别讨论这些要点。

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 方法的回调中的同步错误都会被捕获并传递下去)

通过使用Promise.try,我们将我们的错误处理简化成涵盖了所有的同步错误的错误处理,而不仅仅是那些在第一次异步操作之后的错误处理(正如.then的回调函数)。

Promises/A+

在我们继续讨论下一点之前,让我们来看看 Promises / A+ 是什么,以及它在生态系统中扮演的角色。 Promises / A+ 官网如下总结它:

实现者为实现者提供的一个可靠的,可互操作的开放的 JavaScript Promise 标准。

换句话说,这是一种确保 Promise 的不同实现形式(Bluebird,ES6,Q,RSVP,……)完美协同工作的方法。这个 Promises / A+ 规范就是为什么你可以使用任何你喜欢的 Promises 实现形式,以及为什么你不必关心第三方库(比如 Knex)使用哪种 Promises 实现形式的原因。

为了说明 Promises / A+ 为用户做了什么:
clipboard.png
所有用红色高亮的函数返回 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而使用常见缩进样式的情况下直观浏览代码的方法:
clipboard.png
而这是使用Promise.try的同样代码:
clipboard.png
尽管代码看起来有些“吵闹”,但仍然可以更容易地快速了解代码,只是因为你的眼睛可以“寻找”更少。

怎么实现 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/...


宗介
177 声望14 粉丝