AngularJS 中的 Promise 和 设计模式

其实在 Javascript 中,有另外一种异步处理模式:更屌,在 Javascript 里面经常被叫做Promises, CommonJS 标准委员会于是发布了一个规范,就把这个 API 叫做Promises了。

Promise 背后的概念非常简单,有两部分:

1、Deferreds,定义工作单元

2、Promises,从 Deferreds 返回的数据

AngularJS 中的 Promise 和 设计模式_第1张图片

基本上,你会用 Deferred 作为通信对象,用来定义工作单元的开始,处理和结束三部分。

Promise 是 Deferred 响应数据的输出;它有状态 (等待,执行和拒绝),以及句柄,或叫做回调函数,反正就是那些在 Promise 执行,拒绝或者提示进程中会被调用的方法。

Promise 不同于回调的很重要的一个点是,你可以在 Promise 状态变成执行(resolved)追加处理句柄。这就允许你传输数据,而忽略它是否已经被应用获取,然后缓存它,等等之类的操作,因此你可以对数据执行操作,而不管它是否已经或者即将可用。

在之后的文章中,我们将会基于 AngularJS 来讲解 Promises 。AngularJS 的整个代码库很大程度上依赖于 Promise,包括框架以及你用它编写的应用代码。AngularJS 用的是它自己的 Promises 实现,$q服务,又一个 Q 库的轻量实现。

$q实现了上面提到的所有 Deferred / Promise 方法,除此之外$q还有自己的实现:$q.defer(),用来创建一个新的 Deferred 对象;$q.all(),允许等待多 Promises 执行终了,还有方法$q.when()和$q.reject(),具体我们之后会讲到。

$q.defer()返回一个 Deferred 对象,带有方法resolve(),reject(), 和notify()。Deferred 还有一个promise属性,这是一个promise对象,可以用于应用内部传递。

promise 对象有另外三个方法:.then(),是唯一 Promise 规范要求的方法,用三个回调方法作为参数;一个成功回调,一个失败回调,还有一个状态变化回调。

$q在 Promise 规范之上还添加了两个方法:catch(),可以用于定义一个通用方法,它会在 promise 链中有某个 promise 处理失败时被调用。还有finally(),不管 promise 执行是成功或者失败都会执行。注意,这些不应该和 Javascript 的异常处理混淆或者并用: 在 promise 内部抛出的异常,不会被catch()俘获。(※貌似这里我理解错了)

Promise 简单例子

下面是使用$q,Deferred,和Promise放一起的简单例子。首先我要声明,本文中所有例子的代码都没有经过测试;而且也没有正确的引用Angular服务和依赖,之类的。不过我觉得对于启发你怎么玩,已经够好了。

首先,我们先创建一个新的工作单元,通过 Deferred 对象,用$q.defer():

然后,我们从 Deferred 拿到promise,给它追加一些行为。

AngularJS 中的 Promise 和 设计模式_第2张图片

最后,我们假装做点啥,然后告诉 deferred 我们已经完成了:

当然,这不需要真的异步,所以我们可以用 Angular 的$timeout服务(或者 Javascript 的setTimeout,不过,在 Angular 应用中最好用$timeout,这样你可以 mock/test 它)来假装一下。

好了,有趣的是:我们可以追加很多个then()到一个 promise 上,以及我们可以在 promise 被 resolved 之后追加then():

AngularJS 中的 Promise 和 设计模式_第3张图片

那,要是发生异常怎么办?我们用deferred.reject(),它会出发then()的第二个函数,就像回调一样。

AngularJS 中的 Promise 和 设计模式_第4张图片

不用then()的第二个参数,还有另外一种选择,你可以用链式的catch(),在 promise 链中发生异常的时候它会被调用(可能在很多链之后)。

AngularJS 中的 Promise 和 设计模式_第5张图片

作为一个附加,对于长耗时的处理(比如上传,长计算,批处理,等等),你可以用deferred.notify()作为then()第三个参数,给 promise 一个监听来更新状态。

AngularJS 中的 Promise 和 设计模式_第6张图片

链式 Promise

之前我们已经看过了,你可以给一个 promise 追加多个处理(then())。Promise API 好玩的地方在于允许链式处理:

举个简单的例子,这允许你把你的函数调用切分成单纯的,单一目的方法,而不是一揽子麻团;还有另外一个好处是你可以在多 promise 任务中重用这些方法,就像你执行链式方法一样(比如说任务列表之类的)。

如果你用前一个异步执行结果出发下一个异步处理,那就更牛X了。默认的,一个链式,像上面演示的那种,是会把前一个执行结果对象传递给下一个then()的。比如:

AngularJS 中的 Promise 和 设计模式_第7张图片

这会在控制台输出以下结果:

虽然例子简单,但是你有没有体会到如果then()返回另一个 promise 那种强大。这种情况下,下一个then()会在 promise 完结的时候被执行。这种模式可以用到把 HTTP 请求串上面,比如说(当一个请求依赖于前一个请求的结果的时候):

AngularJS 中的 Promise 和 设计模式_第8张图片

总结:

1、Promise 链会把上一个then的返回结果传递给调用链的下一个then(如果没有就是 undefined)

2、如果then回掉返回一个 promise 对象,下一个then只会在这个 promise 被处理结束的时候调用。

3、在链最后的catch为整个链式处理提供一个异常处理点

4、在链最后的finally总是会被执行,不管 promise 被处理或者被拒绝,起清理作用

Parallel Promises And 'Promise-Ifying' Plain Values


我还提到了$q.all(),允许你等待并行的 promise 处理,当所有的 promise 都被处理结束之后,调用共同的回调。在 Angular 中,这个方法有两种调用方式: 以Array方式或Object方式。Array方式接收多个 promise ,然后在调用.then()的时候使用一个数据结果对象,在结果对象里面包含了所有的 promise 结果,按照输入数组的顺序排列:

第二种方式是接收一个 promise 集合对象,允许你给每个 promise 一个别名,在回调函数中可以使用它们(有更好的可读性):

我建议使用数组表示法,如果你只是希望可以批处理结果,就是说,如果你把所有的结果都平等处理。而以对象方式来处理,则更适合需要自注释代码的时候。

另一个有用的方法是$q.when(),如果你想通过一个普通变量创建一个 promise ,或者你不清楚你要处理的对象是不是 promise 时非常有用。

AngularJS 中的 Promise 和 设计模式_第9张图片

$q.when()在诸如服务中的缓存这种情况也很好用:

AngularJS 中的 Promise 和 设计模式_第10张图片

然后可以这样调用它:

AngularJS 中的实际应用

在 Angular 的 I/O 中,大多数会返回 promise 或者 promise-compatible(then-able)对象,但是,都挺奇怪的。$http文档说,它会返回一个HttpPromise对象,嗯,确实是 promise,但是有两个额外的(有用的)方法,应该不会吓到 jQuery 用户。它定义了success()和error(),用来分别对应then()的第一和第二个参数。

Angular 的$resource服务,用于 REST-endpoints 的$http封装,同样有点奇怪;通用方法(get(),save()之类的四个)接收第二和第三个参数作为success和error回调,同时它们还返回一个对象,当请求被处理之后,会往其中填充请求的数据。它不会直接返回 promise 对象;相反,通过get()方法返回的对象有一个属性$promise,用来暴露 promise 对象。

一方面,这和$http不符,并且 Angular 的所有东西都是/应该是 promise,不过另一方面,它允许开发者简单的把$resource.get()的结果指派给$scope。原先,开发者可以给$scope指定任何 promise,但是从 Angular 1.2 开始被定义为过时了:请看this commit where it was deprecated。

我个人来说,我更喜欢统一的 API,所以我把所有的 I/O 操作都封装到了Service中,统一返回一个promise对象,不过调用$resource有点糙。下面是个例子:

AngularJS 中的 Promise 和 设计模式_第11张图片

这个例子有点晦涩,因为传递 id 参数给BarResource看起来有点多余,不过它也还是有道理的,比如你有一个复杂的对象,但只需要用它的 ID 属性来调用一个服务。上面的好处还在于,在你的 controller 中,你知道从Service返回来的所有东西都是promise对象;你不需要担心它到底是 promise 还是 resouce 或者是HttpPromise,这能让你的代码更加一致,并且可预测 - 因为 Javascript 是弱类型,并且到目前为止,据我所知没有任何一款 IDE 能告诉你方法返回值的类型,它只能告诉你开发者写了什么注释,这点上面就非常重要了。

实际链式例子

我们的代码库有一部分是依赖于前一个调用的结果来执行的。Promise 非常适用这种情况,并且允许你书写易于阅读的代码,尽可能保持你的代码整洁。考虑如下例子:

AngularJS 中的 Promise 和 设计模式_第12张图片

联合异步获取数据(customers,carts,创建checkout)和处理同步数据(calculateTotals);这个实现不知道,甚至不需要知道这些服务是不是异步的,它会等到方法之行结束,不论异步与否。在这个例子中,getCart()会从本地存储中获取数据,createCheckout()会执行一个 HTTP 请求来确定产品的采购,诸如此类。不过从用户的视角来看(执行这个调用的人),它不会关心这些;这个调用起作用了,并且它的状态非常明了,你只要记住前一个调用会将结果返回传递到下一个then()。

当然,它就是自注释代码,并且很简洁。

测试 Promise - 基于代码

测试 Promise 非常简单。你可以硬测,创建你的测试模拟对象,然后暴露then()方法,这种直接测法。但是,为了让事情简单,我只用了$q来创建promise- 这是一个非常快的库。下面尝试演示如何模拟上面用到过的各种服务。注意,这非常冗长,不过,我还没有找出一个方法来解决它,除了在 promise 之外弄一些通用的方法(指针看起来更短更简洁,会比较受欢迎)。

AngularJS 中的 Promise 和 设计模式_第13张图片

你看到咯,测试promise的代码比它自己本身要长十倍;我不知道是否/或者有更简单的代码能达到同样目的,不过,也许这里应该还有我没找到(或者发布)的库。

要获取完整的测试覆盖,需要为三个部分都编写测试代码,从失败到处理结束,一个接一个,确保异常被记录。虽然代码中没有很清楚演示,但是代码/处理实际上会有许多分支;每个promise到最后都会被解决或者拒绝;真或假,或者被建立分支。不过,测试的粒度到底是由你决定的。

我希望这篇文章给大家带来一些理解 promise 的启示,以及教会怎样结合 Angular 来使用 promise。我觉得我只摸到了 一些皮毛,包括在这篇文章以及在到目前为止我所做过的 AngularJS 工程上;promise 能够拥有如此简单的 API,如此简单的概念,并且对大多数 Javascript 应用来说,有如此强大的力量和影响有点难以置信。结合高水平的通用方法,代码库,promise 可以让你写出更干净,易于维护和易于扩展的代码;添加一个句柄,改变它,改变实现方式,所有这些东西都很容易,如果你对 promise 的概念已经理解了的话。

从这点考虑,NodeJS 在开发早期就抛弃了 promise 而采用现在这种回调方式,我觉得非常古怪;当然我还没有完全深入理解它,但是看起来好像是因为性能问题,不符合 Node 的原本目标的缘故。如果你把 NodeJS 当成一个底层的库来看的话,我觉得还是很有道理的;有大量的库可以为 Node 添加高级的 promise API(比如之前提到的 Q).

还有一点请记住,这篇文章是以 AngularJS 为基础的,但是,promises和类promise编程方式已经在 Javascript 库中存在好几年了;jQuery,Deferreds 早在 jQuery 1.5 (1月 2011) 就被添加进来。虽然看起来一样,但不是所有插件都能用。

同样,Backbone.js的 Model Api 也暴露了promise在它的方法中(save()之类),但是,以我的理解,它貌似没有沿着模型事件真正的起作用。也有可能我是错的,因为已经有那么一段时间了。

如果开发一个新的 webapp 的时候,我肯定会推荐基于 promise 的前端应用的,因为它让代码看起来非常整洁,特别是结合函数式编程范式。还有更多功能强劲的编程模式可以在Reginald Braithwaite的Javascript Allongé book中找到,你可以从 LeanPub 拿到免费的阅读副本;还有另外一些比较有用的基于 promise 的代码。

你可能感兴趣的:(AngularJS 中的 Promise 和 设计模式)