AngularJS 中的 Promise 和 设计模式
解决 Javascript 异步事件的传统方式是回调函数;调用一个方法,然后给它一个函数引用,当这个方法完结的时候执行这个函数引用。
<!-- lang: js -->
$.get('api/gizmo/42', function(gizmo) {
console.log(gizmo); // or whatever
});
看起来很不错对不对,不过,也有缺点的;首先,合并或者链接多个异步过程超复杂;要么就是大量的模板代码,要么就是嗯哼你懂的回调地狱(一层套一层的回调):
<!-- lang: js -->
$.get('api/gizmo/42', function(gizmo) {
$.get('api/foobars/' + gizmo, function(foobar) {
$.get('api/barbaz/' + foobar, function(bazbar) {
doSomethingWith(gizmo, foobar, bazbar);
}, errorCallback);
}, errorCallback);
}, errorCallback);
明白了吧。其实在 Javascript 中,有另外一种异步处理模式:更屌,在 Javascript 里面经常被叫做 Promises
, CommonJS 标准委员会于是发布了一个规范,就把这个 API 叫做 Promises
了。
Promise 背后的概念非常简单,有两部分:
- Deferreds,定义工作单元,
- Promises,从 Deferreds 返回的数据。
基本上,你会用 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()
:
<!-- lang: js -->
var deferred = $q.defer();
然后,我们从 Deferred 拿到 promise
,给它追加一些行为。
<!-- lang: js -->
var promise = deferred.promise;
promise.then(function success(data) {
console.log(data);
}, function error(msg) {
console.error(msg);
});
最后,我们假装做点啥,然后告诉 deferred 我们已经完成了:
<!-- lang: js -->
deferred.resolve('all done!');
当然,这不需要真的异步,所以我们可以用 Angular 的 $timeout
服务(或者 Javascript 的 setTimeout
,不过,在 Angular 应用中最好用 $timeout
,这样你可以 mock/test 它)来假装一下。
<!-- lang: js -->
$timeout(function() {
deferred.resolve('All done... eventually');
}, 1000);
好了,有趣的是:我们可以追加很多个 then()
到一个 promise 上,以及我们可以在 promise 被 resolved 之后追加 then()
:
<!-- lang: js -->
var deferred = $q.defer();
var promise = deferred.promise;
// assign behavior before resolving
promise.then(function (data) {
console.log('before:', data);
});
deferred.resolve('Oh look we\'re done already.')
// assign behavior after resolving
promise.then(function (data) {
console.log('after:', data);
});
那,要是发生异常怎么办?我们用 deferred.reject()
,它会出发 then()
的第二个函数,就像回调一样。
<!-- lang: js -->
var deferred = $q.defer();
var promise = deferred.promise;
promise.then(function success(data) {
console.log('Success!', data);
}, function error(msg) {
console.error('Failure!', msg);
});
deferred.reject('We failed :(');
不用 then()
的第二个参数,还有另外一种选择,你可以用链式的 catch()
,在 promise 链中发生异常的时候它会被调用(可能在很多链之后)。
<!-- lang: js -->
promise
.then(function success(data) {
console.log(data);
})
.catch(function error(msg) {
console.error(msg);
});
作为一个附加,对于长耗时的处理(比如上传,长计算,批处理,等等),你可以用 deferred.notify()
作为 then()
第三个参数,给 promise 一个监听来更新状态。
<!-- lang: js -->
var deferred = $q.defer();
var promise = deferred.promise;
promise
.then(function success(data) {
console.log(data);
},
function error(error) {
console.error(error);
},
function notification(notification) {
console.info(notification);
}));
var progress = 0;
var interval = $interval(function() {
if (progress >= 100) {
$interval.cancel(interval);
deferred.resolve('All done!');
}
progress += 10;
deferred.notify(progress + '%...');
}, 100)
链式 Promise
之前我们已经看过了,你可以给一个 promise 追加多个处理(then()
)。Promise API 好玩的地方在于允许链式处理:
<!-- lang: js -->
promise
.then(doSomething)
.then(doSomethingElse)
.then(doSomethingMore)
.catch(logError);
举个简单的例子,这允许你把你的函数调用切分成单纯的,单一目的方法,而不是一揽子麻团;还有另外一个好处是你可以在多 promise 任务中重用这些方法,就像你执行链式方法一样(比如说任务列表之类的)。
如果你用前一个异步执行结果出发下一个异步处理,那就更牛X了。默认的,一个链式,像上面演示的那种,是会把前一个执行结果对象传递给下一个 then()
的。比如:
<!-- lang: js -->
var deferred = $q.defer();
var promise = deferred.promise;
promise
.then(function(val) {
console.log(val);
return 'B';
})
.then(function(val) {
console.log(val);
return 'C'
})
.then(function(val) {
console.log(val);
});
deferred.resolve('A');
这会在控制台输出以下结果:
<!-- lang: js -->
A
B
C
虽然例子简单,但是你有没有体会到如果 then()
返回另一个 promise 那种强大。这种情况下,下一个 then()
会在 promise 完结的时候被执行。这种模式可以用到把 HTTP 请求串上面,比如说(当一个请求依赖于前一个请求的结果的时候):
<!-- lang: js -->
var deferred = $q.defer();
var promise = deferred.promise;
// resolve it after a second
$timeout(function() {
deferred.resolve('foo');
}, 1000);
promise
.then(function(one) {
console.log('Promise one resolved with ', one);
var anotherDeferred = $q.defer();
// resolve after another second
$timeout(function() {
anotherDeferred.resolve('bar');
}, 1000);
return anotherDeferred.promise;
})
.then(function(two) {
console.log('Promise two resolved with ', two);
});
总结:
Promise 链会把上一个
then
的返回结果传递给调用链的下一个then
(如果没有就是 undefined)如果
then
回掉返回一个 promise 对象,下一个then
只会在这个 promise 被处理结束的时候调用。在链最后的
catch
为整个链式处理提供一个异常处理点在链最后的
finally
总是会被执行,不管 promise 被处理或者被拒绝,起清理作用Parallel Promises And 'Promise-Ifying' Plain Values
我还提到了 $q.all()
,允许你等待并行的 promise 处理,当所有的 promise 都被处理结束之后,调用共同的回调。在 Angular 中,这个方法有两种调用方式: 以 Array
方式或 Object
方式。Array
方式接收多个 promise ,然后在调用 .then()
的时候使用一个数据结果对象,在结果对象里面包含了所有的 promise 结果,按照输入数组的顺序排列:
<!-- lang: js -->
$q.all([promiseOne, promiseTwo, promiseThree])
.then(function(results) {
console.log(results[0], results[1], results[2]);
});
第二种方式是接收一个 promise 集合对象,允许你给每个 promise 一个别名,在回调函数中可以使用它们(有更好的可读性):
<!-- lang: js -->
$q.all({ first: promiseOne, second: promiseTwo, third: promiseThree })
.then(function(results) {
console.log(results.first, results.second, results.third);
});
我建议使用数组表示法,如果你只是希望可以批处理结果,就是说,如果你把所有的结果都平等处理。而以对象方式来处理,则更适合需要自注释代码的时候。
另一个有用的方法是 $q.when()
,如果你想通过一个普通变量创建一个 promise ,或者你不清楚你要处理的对象是不是 promise 时非常有用。
<!-- lang: js -->
$q.when('foo')
.then(function(bar) {
console.log(bar);
});
$q.when(aPromise)
.then(function(baz) {
console.log(baz);
});
$q.when(valueOrPromise)
.then(function(boz) {
// well you get the idea.
})
$q.when()
在诸如服务中的缓存这种情况也很好用:
<!-- lang: js -->
angular.module('myApp').service('MyService', function($q, MyResource) {
var cachedSomething;
this.getSomething = function() {
if (cachedSomething) {
return $q.when(cachedSomething);
}
// on first call, return the result of MyResource.get()
// note that 'then()' is chainable / returns a promise,
// so we can return that instead of a separate promise object
return MyResource.get().$promise
.then(function(something) {
cachedSomething = something
});
};
});
然后可以这样调用它:
<!-- lang: js -->
MyService.getSomething()
.then(function(something) {
console.log(something);
});
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
有点糙。下面是个例子:
<!-- lang: js -->
angular.module('fooApp')
.service('BarResource', function ($resource) {
return $resource('api/bar/:id');
})
.service('BarService', function (BarResource) {
this.getBar = function (id) {
return BarResource.get({
id: id
}).$promise;
}
});
这个例子有点晦涩,因为传递 id 参数给 BarResource
看起来有点多余,不过它也还是有道理的,比如你有一个复杂的对象,但只需要用它的 ID 属性来调用一个服务。上面的好处还在于,在你的 controller 中,你知道从 Service
返回来的所有东西都是 promise
对象;你不需要担心它到底是 promise 还是 resouce 或者是 HttpPromise
,这能让你的代码更加一致,并且可预测 - 因为 Javascript 是弱类型,并且到目前为止,据我所知没有任何一款 IDE 能告诉你方法返回值的类型,它只能告诉你开发者写了什么注释,这点上面就非常重要了。
实际链式例子
我们的代码库有一部分是依赖于前一个调用的结果来执行的。Promise 非常适用这种情况,并且允许你书写易于阅读的代码,尽可能保持你的代码整洁。考虑如下例子:
<!-- lang: js -->
angular.module('WebShopApp')
.controller('CheckoutCtrl', function($scope, $log, CustomerService, CartService, CheckoutService) {
function calculateTotals(cart) {
cart.total = cart.products.reduce(function(prev, current) {
return prev.price + current.price;
};
return cart;
}
CustomerService.getCustomer(currentCustomer)
.then(CartService.getCart) // getCart() needs a customer object, returns a cart
.then(calculateTotals)
.then(CheckoutService.createCheckout) // createCheckout() needs a cart object, returns a checkout object
.then(function(checkout) {
$scope.checkout = checkout;
})
.catch($log.error)
});
联合异步获取数据(customers
, carts
,创建 checkout
)和处理同步数据(calculateTotals
);这个实现不知道,甚至不需要知道这些服务是不是异步的,它会等到方法之行结束,不论异步与否。在这个例子中,getCart()
会从本地存储中获取数据, createCheckout()
会执行一个 HTTP 请求来确定产品的采购,诸如此类。不过从用户的视角来看(执行这个调用的人),它不会关心这些;这个调用起作用了,并且它的状态非常明了,你只要记住前一个调用会将结果返回传递到下一个 then()
。
当然,它就是自注释代码,并且很简洁。
测试 Promise - 基于代码
测试 Promise 非常简单。你可以硬测,创建你的测试模拟对象,然后暴露 then()
方法,这种直接测法。但是,为了让事情简单,我只用了 $q
来创建 promise
- 这是一个非常快的库。下面尝试演示如何模拟上面用到过的各种服务。注意,这非常冗长,不过,我还没有找出一个方法来解决它,除了在 promise 之外弄一些通用的方法(指针看起来更短更简洁,会比较受欢迎)。
<!-- lang: js -->
describe('The Checkout controller', function() {
beforeEach(module('WebShopApp'));
it('should do something with promises', inject(function($controller, $q, $rootScope) {
// create mocks; in this case I use jasmine, which has been good enough for me so far as a mocking library.
var CustomerService = jasmine.createSpyObj('CustomerService', ['getCustomer']);
var CartService = jasmine.createSpyObj('CartService', ['getCart']);
var CheckoutService = jasmine.createSpyObj('CheckoutService', ['createCheckout']);
var $scope = $rootScope.$new();
var $log = jasmine.createSpyObj('$log', ['error']);
// Create deferreds for each of the (promise-based) services
var customerServiceDeferred = $q.defer();
var cartServiceDeferred = $q.defer();
var checkoutServiceDeferred = $q.defer();
// Have the mocks return their respective deferred's promises
CustomerService.getCustomer.andReturn(customerServiceDeferred.promise);
CartService.getCart.andReturn(cartServiceDeferred.promise);
CheckoutService.createCheckout.andReturn(checkoutServiceDeferred.promise);
// Create the controller; this will trigger the first call (getCustomer) to be executed,
// and it will hold until we start resolving promises.
$controller("CheckoutCtrl", {
$scope: $scope,
CustomerService: CustomerService,
CartService: CartService,
CheckoutService: CheckoutService
});
// Resolve the first customer.
var firstCustomer = {id: "customer 1"};
customerServiceDeferred.resolve(firstCustomer);
// ... However: this *will not* trigger the 'then()' callback to be called yet;
// we need to tell Angular to go and run a cycle first:
$rootScope.$apply();
expect(CartService.getCart).toHaveBeenCalledWith(firstCustomer);
// setup the next promise resolution
var cart = {products: [ { price: 1 }, { price: 2 } ]}
cartServiceDeferred.resolve(cart);
// apply the next 'then'
$rootScope.$apply();
var expectedCart = angular.copy(cart);
cart.total = 3;
expect(CheckoutService.createCheckout).toHaveBeenCalledWith(expectedCart);
// Resolve the checkout service
var checkout = {total: 3}; // doesn't really matter
checkoutServiceDeferred.resolve(checkout);
// apply the next 'then'
$rootScope.$apply();
expect($scope.checkout).toEqual(checkout);
expect($log.error).not.toHaveBeenCalled();
}));
});
你看到咯,测试 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 和 设计模式的更多相关文章
- AngularJS 中的 Promise 和 设计模式(转)
原文地址:http://my.oschina.net/ilivebox/blog/293771 目录[-] Promise 简单例子 链式 Promise Parallel Promises And ...
- Angularjs中的promise
promise 是一种用异步方式处理值的方法,promise是对象,代表了一个函数最终可能的返回值或抛出的异常.在与远程对象打交道非常有用,可以把它们看成一个远程对象的代理. 要在Angular中创建 ...
- AngularJS 中的Promise --- $q服务详解
先说说什么是Promise,什么是$q吧.Promise是一种异步处理模式,有很多的实现方式,比如著名的Kris Kwal's Q还有JQuery的Deffered. 什么是Promise 以前了解过 ...
- angularJS中的Promise对象($q)的深入理解
原文链接:a better way to learn AngularJS - promises AngularJS通过内置的$q服务提供Promise编程模式.通过将异步函数注册到promise对象, ...
- angularJS中XHR与promise
angularJS应用是完全运行在客户端的应用,我们可以通过angularJS构建一个不需依赖于后端,同时能够实现动态内容和响应的web应用,angularJS提供了将应用与远程服务器的信息集成在一起 ...
- AngularJS中处理多个promise
在使用AngularJS中处理promise的时候,有时会碰到需要处理多个promise的情况. 最简单的处理就是每个promise都then.如下: var app = angular.module ...
- 深入理解jQuery、Angular、node中的Promise
最初遇到Promise是在jQuery中,在jQuery1.5版本中引入了Deferred Object,这个异步队列模块用于实现异步任务和回调函数的解耦.为ajax模块.队列模块.ready事件提供 ...
- AngularJS中实现无限级联动菜单(使用demo)
昨天没来得及贴几个使用demo,今天补上,供有兴趣的同学参考 :) 1. 同步加载子选项demo2. 异步加载子选项demo3. 初始值回填demo4. 倒金字塔依赖demo directive的源代 ...
- AngularJS中实现无限级联动菜单
多级联动菜单是常见的前端组件,比如省份-城市联动.高校-学院-专业联动等等.场景虽然常见,但仔细分析起来要实现一个通用的无限分级联动菜单却不一定像想象的那么简单.比如,我们需要考虑子菜单的加载是同步的 ...
随机推荐
- activiti笔记一:流程图xml文件
一. 流程图有两个基本要素组成:点和线 点:开始节点.结束节点.各种网关.各种事件 线:连接两点之间的sequenceFlow 二. 一般包含三个步骤:部署.启动.完成 三. 启动的时候,可以设置流程 ...
- Oracle中MERGE语句的使用
Oracle在9i引入了merge命令, 通过这个merge你能够在一个SQL语句中对一个表同时执行inserts和updates操作. 当然是update还是insert是依据于你的指定的条件判断的 ...
- shell脚本分类
shell脚本分为三类:登录脚本.交互式脚本.非交互式脚本 一. 登录脚本类似于windows下的计算机设置中的登录脚本和账户设置下的登录脚本的合集(我是这么理解的哈). 其配置文件的关键词为pref ...
- New Start!
博客连续搬了几个地方,从一开始的网易lofter,到点点,再到后来新浪sae自己搭建,折腾了很久,都因为各种原因没有坚持写博. 想了想,其实说到底还是因为懒,根本原因是自己没有毅力.什么不能贴代码,不 ...
- 使用Qt编写服务器端程序(包括Http传输服务器端)的方法
使用Qt编写客户端的程序的示例或demo较多,但是编写服务器端程序的demo很少.当然,服务器端的程序一般不需要带界面,这点我们可以理解.不过有些时候我们还是需要使用Qt编写一个简单的测试用的服务器代 ...
- Asp.net MVC Web.config配置技巧
一.视图引入命名空间的设置 之前经常写这样的代码,而且每个页面都要写: @model IEnumerable<MvcStart.Models.People_Model> 其实有一种很方便的 ...
- 绘图工具graphviz学习使用
画图工具: http://www.tuicool.com/articles/r2iAfa http://www.tuicool.com/articles/RjQfey 绘图工具graphviz学习使用 ...
- GRUB启动管理器
Linux学习笔记之 5 Linux GRUB启动管理器 1.GRUB简介 1.1grub与启动引导器 启动引导器是计算机启动过程中运行的第一个真正的软件,通常计算机启动时在通过BIOS自检后 ...
- UESTC_秋实大哥与花 2015 UESTC Training for Data Structures<Problem B>
B - 秋实大哥与花 Time Limit: 3000/1000MS (Java/Others) Memory Limit: 65535/65535KB (Java/Others) Submi ...
- Pascal's Triangle 解答
Question Given numRows, generate the first numRows of Pascal's triangle. For example, given numRows ...