什么是 Promise.try,它为何重要?

avatar
UX @京东
原文链接: jdc.jd.com

原文地址 cryto.net/~joepie91/b…

在#Node.js#频道里经常困扰大家的一个话题是Bluebird提供的Promise.try方法。大家并不清楚该方法的功能也不知道为何要使用它。同时,几乎所有的关于Promsie的指南中针对该方法错误的演示使得这种情况没有任何改善。

在本文中,我会尝试解释究竟什么是Promise.try以及为何你应该使用它。我假设你已经对Promise有所了解并且知道.then在Promise中的作用。

即使你在使用一个不同的Promsie实现(例如ES6 Promise),本文还是可以帮到你。文章末尾我会解释如何在非Bluebird环境中实现相同的功能。

究竟什么是Promise.try呢?

简单来说,除了不需要跟在一个前置Promise之后以外,Promise.try很像.then。这么说还是有一些含糊不清,所以让我们先看一个示例。

以下是一段典型的Promise使用场景:

	
functiongetUsername(userId){

returndatabase.users.get({id:userId})

.then(function(user){

returnuser.name;

});

到目前为止,一切顺利。我们假设database.users.get会返回一个Promise,并且该Promise最终会返回一个带有name属性的对象。

以下是同样的代码,但是引入了Promise.try

	
varPromise=require("bluebird");

functiongetUsername(userId){

returnPromise.try(function(){

returndatabase.users.get({id:userID});

}).then(function(user){

returnuser.name;

});

可以看到,我们的调用链以Promise.try而不是database.users.get开始。像使用.then一样,我们执行Promise.try方法并传递给它一个直接返回database.users.get调用的函数。

这样做有什么意义呢?

以上的代码看起来似乎是多余的。但实际上它有以下几个优点:

  1. 更好的错误处理 同步代码中的异常不论出现在何处都会以rejection的形式向Promise链后端传递。
  2. 更好的兼容性 你可以始终使用你自己喜欢的Promise实现,而不用担心第三方代码在使用哪个。
  3. 更好的代码阅读体验 所有的代码在水平方向上将处于同一个缩进层级,这将使你阅读代码变得更容易。

接下来我会逐一介绍这些优点:

1. 更好的错误处理

Promise的一个被大力宣扬的优点就是用户可以用同一种方式同时处理同步异常和异步异常 —— 同步异常会被捕获并且会作为一个rejected Promise向后传递。但事实真的是这样吗?让我们看看以下这个上文示例的小变种:

	
functiongetUsername(userId){

returndatabase.users.get({id:userId})

.then(function(user){

returnuesr.name;

});

在这个改动后的版本中,我们在输入user.name的时候手误,现在它成了uesr.name。由于uesr是未定义的,所以它不可能有任何属性,因此该行代码会产生异常。接着就如你期望的一样,这个异常会被.then捕获并被转变成一个rejected Promise。

但是如果database.users.get同步的抛出了异常呢?如果在第三方的代码里存在拼写错误或是其他问题呢?Promises的错误捕获机制的前提是使用者编写的所有同步代码都要放在.then中,这样它就可以将这些同步代码放入一个try/catch块中执行。

但是…我们代码中的database.users.get并不在.then块中。因此Promise就不能访问这部分代码也就不能将它们包裹在try/catch中。这就导致同步异常不能被转变为异步异常。我们又回到了原点,不得不处理两种形式的异常 —— 同步的和异步的。

现在,让我们回过头来看看使用了Promise.try的示例:

varPromise=require("bluebird");

functiongetUsername(userId){

returnPromise.try(function(){

returndatabase.users.get({id:userID});

}).then(function(user){

returnuser.name;

});

我前边说过Promise.try很像.then,但是它不需要跟在Promise后边。除此之外,它还会捕获database.users.get中的同步异常,就像.then一样!

通过引入Promise.try,我们已经把代码的错误处理改造为可以覆盖所有的同步异常,而不是只能捕获第一次异步操作(第一次.then调用)之后的异常了。

补充信息: Promises/A+

在我们继续讨论Promise.try的下一个优点之前,让我们看看什么是Promises/A+以及它扮演的角色。Promises/A+网站上对它的介绍是:

An open standard for sound, interoperable JavaScript promises—by implementers, for implementers.

换种说法,它是一份可以保证不同的Promises实现(Bluebird,ES6,Q,RSVP, …)之间可以无缝衔接的规范。Promises/A+规范就是为什么你可以选择任何你喜欢的Promise实现,而不用担心第三方代码库(例如Knex)使用何种Promise实现的原因。

下图演示了Promises/A+对用户的帮助:

Visual illustration of Promises/A+

示例中所有红色高亮的函数都返回Bluebird Promises,蓝色高亮的返回ES6 Promise,绿色高亮的返回Q Promise。

需要注意的是即使在第一个回调函数中我们返回了一个ES6 Promise,包裹它的.then方法还是会返回一个Bluebird Promise。同样的,即使第二个回调函数返回的是一个Q Promise,我们的doStuff函数还是会返回一个Bluebird Promise。

出现这种结果的原因是,除了捕获同步异常以外,.then还会对回调函数的返回值进行处理以返回一个和.then自身来自同一个实现的Promise对象。实际上,这就意味着调用链中的第一个Promise决定了后续代码中你会与哪种Promise实现打交道。

这是一种保证API一致性的实用方法。你只需要关心整个调用链第一个方法所使用的Promise实现,而不用担心后续代码使用何种实现。

2. 更好的兼容性

但是以上描述的行为并不总能符合预期,让我们看看下边的示例:

varPromise=require("bluebird");

functiongetAllUsernames(){

// This will return an ES6 Promise.

returndatabase.users.getAll()

.map(function(user){

returnuser.name;

});

如果你对map不熟悉并想了解更多信息,可以参考这篇文章

在这个例子中使用的.map方法是一个Bluebird的专有特性,在ES6 Promises上并不可用。由于调用链的第一个函数(database.users.getAll)返回的是一个ES6 Promise,这就导致我们在这里并不能访问这个方法。哪怕是在项目中其它位置使用了Bluebird也不行。

现在让我们再看看这个示例,只不过这次我们使用了Promise.try

varPromise=require("bluebird");

functiongetAllUsernames(){

// This will return a Bluebird Promise.

returnPromise.try(function(){

// This will return an ES6 Promise.

returndatabase.users.getAll();

}).map(function(user){

returnuser.name;

});

现在我们就可以使用.map了!因为我们以来自Bluebird的Promise.try开始了调用链,这就使得所有后续的Promises都成为了Bluebird Promises。因此我们不必再关心每个.then回掉函数里发生了什么。

通过像这样引入Promise.try,你就可以决定在整个调用链中采用哪种实现的Promise,只要保证调用链里第一个返回的Promise是你想要的类型就可以了。你无法通过.then实现相同的效果,因为.then需要跟在一个前置的Promise后边,而实际情况往往是你并不能决定这个前置Promise是来自那种Promise实现的。

3. 更好的代码阅读体验

最后一个优点与可读性有关。通过使用Promise.try来开启每一个调用链,所有的“业务逻辑”都会在水平方向出现在同一个缩进层级上。虽然这个优点看起来微不足道,但由于人类浏览大段文字的方式,它实际上能起到很大的作用。

让我们通过图片来解释这里的差别,下图展示的是你如何阅读没有使用Promise.try时的代码:

Visual scanning without Promise.try

… 然后是同样的代码,但是使用了Promise.try:

Visual scanning with Promise.try

虽然这使得代码变得看起来稍微有些“复杂”,但它可以帮你更快的理解代码的主要逻辑,因为你的眼睛不再需要左右寻找缩进。

如果我使用的不是Bluebird呢?

据我所知,Bluebird是目前唯一自带Promise.try方法的Promise 实现。然而将该功能复制到其他Promise实现是相当容易的,只要这个Promise实现的new Promise可以捕获同步异常即可。

例如,es6-promise-try是我为ES6 Promises提供的一种实现,它同时也可以在浏览器中工作。我还没来得及为它编写文档。但它的使用方式本质上和Promise.try完全相同,只不过方法名变成了promiseTry,示例如下:

	
varpromiseTry=require("es6-promise-try");

functiongetUsername(userId){

returnpromiseTry(function(){

returndatabase.users.get({id:userID});

}).then(function(user){

returnuser.name;

});

需要注意的是es6-promise-try默认假设你的运行环境中支持ES6 Promise。如果还不支持,那么你需要引入类似es6-promise的polyfill脚本。

由于Bluebird具有健壮的错误处理机制和强大的调试功能,我强烈建议你优先考虑它。通常情况下,ES6 Promises只有在受限环境中才会成为你的首选,例如需要兼容低版本IE浏览器或是尝试降低前端文件大小时。

如果你知道有其他Promise库也实现了Promise.try的功能,请联系我。我会把它们列在这里。我的联系方式在页面底部可以找到。

我目前正在提供Node.js代码评审和辅导服务。了解更多信息请点击这里