什么是promise
promise
是一种用异步方式处理值(或非值)的方法。promise
是对象,代表了一个函数最终可能的返回值或者抛出的异常。
习惯上,JS使用闭包或者回调来响应非同步的有意义的数据,比如页面加载之后的XHR请求。我们可以跟数据进行交互,就好像它已经返回了一样,而不需要依赖于回调函数的触发。
回调使得调用不一致,得不到保证,当依赖于其他回调时,它们篡改代码的流程,通常会让调试变得非常难。每一步调用之后,都需要显式处理错误。
在执行异步方法时触发一个函数,然后期待一个回调能运行起来。与之不同的是,promise
提供了另外一种抽象:这些函数返回promise
对象。
这个回调金字塔已经失控了,而且我们还没有加入健壮的错误处理代码。此外,在被调用的回调内部,也需要知道参数的顺序。
刚才基于promise
版本的代码看上去更接近于:
代码不仅仅是可读性变高了,也更容易理解了。我们可以保证回调是一个值,而不用处理回调接口。
为什么使用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
请求。
如果有服务返回了一个promise
,可以在.then()
方法中与这个promise
交互,它允许我们修改作用域上的任意变量,放置到视图上,并且期望AngularJS会为我们执行它。
当对getPullRequests
的异步调用返回时, 在.then()
方法中就可以用$scope.pullRequests
这个值了,然后它会更新$scope.pullRequests
数组。
如何创建一个promise
在AngularJS中创建promise
,可以使用内置的$q
服务。$q
服务在它的deferred
API中提供了一些方法。
首先,需要把$q
服务注入到想要使用它的对象中。
要创建一个deferred
对象,可以调用defer()
方法。
deferred
对象暴露了三个方法,以及一个可以用于处理promise
的promise
属性。
resolve(value):
resolve
函数用这个值来执行deferred promise
。
reject(reason)
这个方法用一个原因来拒绝deferred promise
。它等同于使用一个“拒绝”来执行一个promise
。
notify(value):这个方法用
promise
的执行状态来进行响应。
如果我们要从promise
返回一个状态,可以使用notify()
函数来传送它。假设我们想要从一个promise
创建多个长时间运行的请求。可以调用notify
函数发回一个过程通知。
有了GithubService
对象上的这个makeMultipleRequests()
函数,每次获取和处理一个仓库时,都会收到一个过程通知。
可以在我们对promise
的使用中用到这个通知,在用promise
时加上第三个函数调用。
可以在deferred
对象上以属性的方式访问promise
:deferred.promise
。
上面这个例子展示了如何创建一个函数用于响应promise
,看上去可能类似于下面这些GithubService
上的方法。
现在我们就可以用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
回调。
finally(callback)
finally
方法允许我们观察promis
e的履行或者拒绝,而无需修改结果的值。当我们需要释放一个资源,或者是运行一些清理工作,不管promise
是成功还是失败时,这个方法会很有用。
我们不能直接调用这个方法,因为finally
是IE中JS的一个保留字。纠结到最后,只好这样调用它了:
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
的执行。这个中断也是$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
一样使用它。