什么是promise
promise
是一种用异步方式处理值(或非值)的方法。promise
是对象,代表了一个函数最终可能的返回值或者抛出的异常。
习惯上,JS使用闭包或者回调来响应非同步的有意义的数据,比如页面加载之后的XHR请求。我们可以跟数据进行交互,就好像它已经返回了一样,而不需要依赖于回调函数的触发。
回调使得调用不一致,得不到保证,当依赖于其他回调时,它们篡改代码的流程,通常会让调试变得非常难。每一步调用之后,都需要显式处理错误。
在执行异步方法时触发一个函数,然后期待一个回调能运行起来。与之不同的是,promise
提供了另外一种抽象:这些函数返回promise
对象。
// 示例回调代码
User.get(fromId, {
success: function(err, user) {
if (err) return {error: err};
user.friends.find(toId, function(err, friend) {
if (err) return {error: err};
user.sendMessage(friend, message, callback);
});
},
fail: function(err) {
return {error: err}
}
});
这个回调金字塔已经失控了,而且我们还没有加入健壮的错误处理代码。此外,在被调用的回调内部,也需要知道参数的顺序。
刚才基于promise
版本的代码看上去更接近于:
User.get(fromId)
.then(function(user) {
return user.friends.find(toId);
}, function(err) {
// 没找到用户
})
.then(function(friend) {
return user.sendMessage(friend, message);
}, function(err) {
// 用户的朋友返回了异常
})
.then(function(success) {
// user was sent the message
}, function(err) {
// 发生错误了
});
代码不仅仅是可读性变高了,也更容易理解了。我们可以保证回调是一个值,而不用处理回调接口。
为什么使用promise
promise
让异步函数看上去像同步的。基于同步函数,我们可以按照预期来捕获返回值和异常值。
可以在程序中的任何时刻捕捉错误,并且绕过依赖于程序异常的后续代码。
因此,使用promise
的目的是:获得功能组合和错误冒泡能力的同时,保持代码异步运行的能力。
promise
是头等对象,自带了一些约定。
- 只有一个
resolve
或者reject
会被调用到:-
resolve
被调用时,带有一个履行值; -
reject
被调用时要带一个拒绝原因。
-
- 如果
promise
被执行或者拒绝了,依赖于它们的处理程序仍然会被调用; - 处理程序总是会被异步调用。
此外,可以把promise
串起来,并且允许代码以通常运行的方式来处理。从一个promise
冒出的异常会贯穿整个promise
链。promise
总是异步执行的,可以放心使用,无需担心它们会阻塞应用的其他部分。
AngularJS中的promise
AngularJS的事件循环给予了AngularJS特有的能力,能在$rootScope.$evalAsync
阶段中执行promise
。promise
会坐等$digest
运行循环结束。这件事让我们能毫无压力地把promise
的结果转换到视图上。它也能让我们不加思考地把XHR调用的结果直接赋值到$scope
对象的属性上。
例子:从GitHub上返回一组针对AngularJS的开放pull
请求。
<h1>Open Pull Requests for Angular JS</h1>
<ul ng-controller="DashboardController">
<li ng-repeat="pr in pullRequests">
{{ pr.title }}
</li>
</ul>
如果有服务返回了一个promise
,可以在.then()
方法中与这个promise
交互,它允许我们修改作用域上的任意变量,放置到视图上,并且期望AngularJS会为我们执行它。
angular.module('myApp', [])
.controller('DashboardController', [
'$scope', 'GithubService',
function($scope, UserService) {
// GithubService的getPullRequests()方法
// 返回了一个promise
User.getPullRequests(123)
.then(function(data) {
$scope.pullRequests = data.data;
});
}]);
当对getPullRequests
的异步调用返回时, 在.then()
方法中就可以用$scope.pullRequests
这个值了,然后它会更新$scope.pullRequests
数组。
如何创建一个promise
在AngularJS中创建promise
,可以使用内置的$q
服务。$q
服务在它的deferred
API中提供了一些方法。
首先,需要把$q
服务注入到想要使用它的对象中。
angular.module('myApp',[]).factory('GithubService',['$q',function($q) {
// 现在就可以访问到$q库了
}]);
要创建一个deferred
对象,可以调用defer()
方法。
var deferred = $q.defer();
deferred
对象暴露了三个方法,以及一个可以用于处理promise
的promise
属性。
- resolve(value):
resolve
函数用这个值来执行deferred promise
。
deferred.resolve({name: "Ari", username: "@auser"});
- reject(reason)
这个方法用一个原因来拒绝deferred promise
。它等同于使用一个“拒绝”来执行一个promise
。
deferred.reject("Can't update user");
// 等同于
deferred.resolve($q.reject("Can't update user"));
- notify(value):这个方法用
promise
的执行状态来进行响应。
如果我们要从promise
返回一个状态,可以使用notify()
函数来传送它。假设我们想要从一个promise
创建多个长时间运行的请求。可以调用notify
函数发回一个过程通知。
.factory('GithubService', function($q, $http) {
// 从仓库获取事件
var getEventsFromRepo = function() {
// 任务
}
var service = {
makeMultipleRequests: function(repos) {
var d = $q.defer(),
percentComplete = 0,
output = [];
for (var i = 0; i < repos.length; i++) {
output.push(getEventsFromRepo(repos[i]));
percentComplete = (i+1)/repos.length * 100;
d.notify(percentComplete);
}
d.resolve(output);
return d.promise;
}
}
return service;
});
有了GithubService
对象上的这个makeMultipleRequests()
函数,每次获取和处理一个仓库时,都会收到一个过程通知。
可以在我们对promise
的使用中用到这个通知,在用promise
时加上第三个函数调用。
.controller('HomeController',
function($scope, GithubService) {
GithubService.makeMultipleRequests([
'auser/beehive', 'angular/angular.js'
])
.then(function(result) {
// 处理结果
}, function(err) {
// 发生错误了
}, function(percentComplete) {
$scope.progress = percentComplete;
});
});
可以在deferred
对象上以属性的方式访问promise
:deferred.promise
。
上面这个例子展示了如何创建一个函数用于响应promise
,看上去可能类似于下面这些GithubService
上的方法。
angular.module('myApp', [])
.factory('GithubService', [
'$q', '$http',
function($q, $http) {
var getPullRequests = function() {
var deferred = $q.defer();
// 从Github获取打开的angularjs pull请求列表
$http.get('https://api.github.com/repos/angular/angular.js/pulls')
.success(function(data) {
deferred.resolve(data);
})
.error(function(reason) {
deferred.reject(reason);
})
return deferred.promise;
}
return { // 返回工厂对象
getPullRequests: getPullRequests
};
}]);
现在我们就可以用promise
API来跟getPullRequests() promise
交互。在上面这个service
的实例中,可以用两种不同方式跟promise
交互。
- then(successFn,errFn,notifyFn)
无论promise
成功还是失败了,当结果可用之后,then
都会立刻异步调用successFn
或者errFn
。这个方法始终用一个参数来调用回调函数:结果,或者是拒绝的理由。
在promise
被执行或者拒绝之前,notifyFn
回调可能会被调用0到多次,以提供过程状态的提示。
then()
方法总是返回一个新的promise
,可以通过successFn
或者errFn
这样的返回值执行或者被拒绝。它也能通过notifyFn
提供通知。 - catch(errFn)
这个方法就只是个帮助函数,能让我们用.catch(function(reason){})
取代err
回调。
$http.get('/repos/angular/angular.js/pulls')
.catch(function(reason) {
deferred.reject(reason);
});
- finally(callback)
finally
方法允许我们观察promis
e的履行或者拒绝,而无需修改结果的值。当我们需要释放一个资源,或者是运行一些清理工作,不管promise
是成功还是失败时,这个方法会很有用。
我们不能直接调用这个方法,因为finally
是IE中JS的一个保留字。纠结到最后,只好这样调用它了:
promise['finally'](function() {});
AngularJS的$q deferred
对象是可以串成链的,这样即使是then
,返回的也是一个promise
。这个promise
一被执行,then
返回的promise
就已经是resolved
或者rejected
的了。
这些promise
也就是AngularJS能支持$http
拦截器的原因。
$q
服务类似于原始的Kris Kowal的Q库:
(1) $q
是跟Angular的$rootScope
模型集成的,所以在Angular中,执行和拒绝都很快。
(2) $q promise
是跟Angular模板引擎集成的,这意味着在视图中找到的任何promise
都会在视图中被执行或者拒绝。
(3) $q
很小,所以没有包含Q库的完整功能。
链式请求
then
方法在初始promise
被执行之后,返回一个新的派生promise
。这种返回形式可以把另一个then
接在初始的then
方法结果之后。
// 一个响应promise的服务
GithubService.then(function(data) {
var events = [];
for (var i = 0; i < data.length; i++) {
events.push(data[i].events);
}
return events;
}).then(function(events) {
$scope.events = events;
});
在本例中,我们可以创建一个执行链,它允许我们中断基于更多功能的应用流程,可以籍此导向不同的结果。这个中断能让我们在执行链的任意时刻暂停或者推迟promise
的执行。这个中断也是$http
服务实现请求和响应拦截器的方式。
$q
库自带了几个不同的有用方法。
all(promises)
如果我们有多个promise
,想要把它们合并成一个,可以使用$q.all(promises)
方法来把它
们合并成一个promise
。这个方法带有一个参数。
- promises(数组或者promise对象):一个
promise
数组或者promise
的hash
。
all()
方法返回单个promise
,会执行一个数组或者一个散列的值。每个值会响应promise
散列中的相同序号或者键。如果任意一个promise
被拒绝了,结果的promise
也会被拒绝。
defer()
defer()
方法创建了一个deferred
对象,它没有参数,返回deferred
对象的一个实例。
reject(reason)
这个方法创建了一个promise
,被以某一原因拒绝执行了。它专门用于让我们能在一个promise
链中转发拒绝的promise
,类似JS中的throw
。在同样意义上,我们能在JS中捕获一个异常,也能够转发这个拒绝,我们需要把这个错误重新抛出。可以通过$q.reject(reason)
来做到这点。
这个方法带有单个参数:
- reason(常量、字符串、异常、对象):拒绝的原因。
reject()
方法返回一个已经用某个原因拒绝的promise
。
when(value)
when()
函数把一个可能是值或者能接着then
的promise
包装成一个$q promise
。这样我们就能处理一个可能是也可能不是promise
的对象。
when()
函数有一个参数:value
,该参数是个值,或者是promise
。
when()
函数返回了一个promise
,我们可以像使用其他promise
一样使用它。