翻译:JavaScript Promises and AngularJS $q Service
原文:http://www.webdeveasy.com/javascript-promises-and-angularjs-q-service/
原文时间:2014年9月30号
一个promise(延缓)是处理异步开发简单而强大的方法。CommonJS 维基百科列出了 几个promise模式的实施提议。AngularJS自己的promise实现方法是受kris Kowal's Q的方法启发的。在这篇文章中我会介绍promises,它的目的和怎么通过AngularJS $q的promise服务开发的教程。
Promise(延缓)目的
在JavaScript中,异步方法通常通过调用回调方法来实现成功或失败的处理。比如说浏览器的地理位置api,获取地理坐标时就需要成功或者失败的回调方法。
function success(position) { var coords = position.coords; console.log('Your current position is ' + coords.latitude + ' X ' + coords.longitude); } function error(err) { console.warn('ERROR(' + err.code + '): ' + err.message); } navigator.geolocation.getCurrentPosition(success, error);
另一个例子就是xhr请求(ajax请求),它有一个onreadystatechange回调方法,当readyState改变时就会调用它。
var xhr = new window.XMLHttpRequest(); xhr.open('GET', 'http://www.webdeveasy.com', true); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { console.log('Success'); } } }; xhr.send();
Javascript中还有许多其他的异步例子,下面讨论麻烦的多个异步方法运行。
串行(无尽金字塔)
假设现在有N种异步方法需要串行运行:async1(success, failure)
, async2(success, failure)
, …, asyncN(success, failure),一个接一个执行直到成功,每个方法都有成功和失败的回调,那么代码就是这样:
async1(function() { async2(function() { async3(function() { async4(function() { .... .... .... asyncN(null, null); .... .... .... }, null); }, null); }, null); }, null);
这就是著名的回调金字塔(callback pyramid of doom)。虽然还有更好的写法(把回调流分隔成函数),但是这种方式还是很难读懂和维护。
并行
假设我们有N个异步方法,async1(success, failure)
, async2(success, failure)
, …, asyncN(success, failure)
,我们需要让他们并行运行,再在最后弹出一个消息。每个方法都有成功和失败的回调,那么代码就是这样:
var counter = N; function success() { counter --; if (counter === 0) { alert('done!'); } } async1(success); async2(success); .... .... asyncN(success);
我们声明了一个变量counter,让它的值为N,每当一个方法成功了,就减一,然后检测是否为零,也就是是否执行到最后一个了。这种方法使用起来既麻烦又不利于维护,特别是当每个异步方法还有参数要传到success()方法里的时候,那样我们还要保存每次运行的结果。
在上面两个例子里,在一个异步操作过程中,我们必须要指定成功的回调。也就是说,当我们用回调时,异步操作需要一个引用来进行下一步,但是它的下一步操作也许是与自己无关的。这就导致了很难重复使用和测试的紧密耦合模块和服务。
什么是Promise和Deferred
deferred表示一个异步操作的结果。它含有操作的结果和提供一个可以用来标记状态的接口。它还提供一个连接promise实例的途径。
promise提供一可以和相关deferred互动的接口,这样就可以获取到deferred的结果和状态了。
当创建一个deferred,它的状态是等待中(pending),这时它没有值。当我们resolve()或者reject()这个deferred的时候,它把状态改为已解决(resolved)或者已拒绝(rejected)。我们在创建一个deferred后还是可以立刻获取到promise,设置给它将来的操作结果分配互动操作。这些互动操作只有当deferred解决或者拒绝后才会执行。
如果需要处理多个异步,通过promises我们也可以容易的创建异步操作,即使我们还没决定下一步要做什么。这就是为什么耦合是松散的。一个异步操作不需要下一步的动作,它只需要知道什么时候准备好。
defferred提供方法改变一个操作的状态,而promise只提供需要的方法来处理和解决状态,而不是改变状态的方法。
在不同语言中(JavaScript,JAVA,C++,Python等等)和框架(NodeJS,JQuery)有许多promise实现方法。AngularJS的实现方法就是$q这个服务。
怎么使用Deferrers和Promises
在理解了promises和他们的目的之后,现在我们来看看怎么使用他们。之前提到,promises有多种实现方法,因此,不同的方法对应不同的用途。这里将会介绍AngularJS中的promise实现-$q服务。如果你使用的是其他实现方法,别担心,我会阐述大部分方法,即使没说到,你也可以找到类似的。
基本用法
首先,我们先创建一个deferred
var myFirstDeferred = $q.defer();
简单吧,myFirstDeferred代表一个异步操作结束后可以被解决(resolved)或者拒绝的(rejected)deffered。假设我们有一个带有有成功和失败的回调方法的异步操作:async(success, failure)。当async执行后,我们想要处理它的结果(返回值或者错误信息)。
async(function(value) { myFirstDeferred.resolve(value); }, function(errorReason) { myFirstDeferred.reject(errorReason); });
既然$q的处理方法(resolve(),reject())不需要结果就可以执行,我们也可以简化一下:(注:这里是一个异步前提,还没用到AngularJS特性):
async(myFirstDeferred.resolve, myFirstDeferred.reject);
然后我们来处理promise,把promise从myFirstDeferred中拿出来,这样给它指定回调的操作就简单了(注:这里没用到async呢,这里是AngularJS中$q的用法,then,then代表成功,失败回调,详细往下看):
var myFirstPromise = myFirstDeferred.promise; myFirstPromise .then(function(data) { console.log('My first promise succeeded', data); }, function(error) { console.log('My first promise failed', error); });
记住我们可以在创建deferred后直接指定回调方法,即使在调用async()之前,想指定什么就指定什么:
var anotherDeferred = $q.defer(); anotherDeferred.promise .then(function(data) { console.log('This success method was assigned BEFORE calling to async()', data); }, function(error) { console.log('This failure method was assigned BEFORE calling to async()', error); }); async(anotherDeferred.resolve, anotherDeferred.reject); anotherDeferred.promise .then(function(data) { console.log('This ANOTHER success method was assigned AFTER calling to async()', data); }, function(error) { console.log('This ANOTHER failure method was assigned AFTER calling to async()', error); });
如果async()成功,两个成功的回调都会触发。反之亦然。我推荐的做法是把异步操作封装下,它返回一个promise。这样的话既可以指定回调方法,又不用担心会干涉deferred的状态。
function getData() { var deferred = $q.defer(); async(deferred.resolve, deferred.reject); return deferred.promise; } ... ... // Later, in a different file var dataPromise = getData() ... ... ... // Much later, at the bottom of that file :) dataPromise .then(function(data) { console.log('Success!', data); }, function(error) { console.log('Failure...', error); });
至此,我们通过promises,指定了成功和失败的回调。当然,我们也可以只指定成功或只指定失败回调。
promise.then(function() { console.log('Assign only success callback to promise'); }); promise.catch(function() { console.log('Assign only failure callback to promise'); // 这是promise.then(null, errorCallback)的缩写 });
只调用promise.then()会只指定成功回调,调用proise.catch会调用失败回调。catch()其实是promise.then(null,errorCallback)的缩写。
有时候我们想触发一个回调,无论是成功还是失败,我们就可以用promise.finally()
promise.finally(function() { console.log('Assign a function that will be invoked both upon success and failure'); });
这个等同于:
var callback = function() { console.log('Assign a function that will be invoked both upon success and failure'); }; promise.then(callback, callback);
绑定变量和promises
假设我们有一个返回一个promise的异步方法async(),我们来看下这段有趣的代码:
var promise1 = async(); var promise2 = promise1.then(function(x) { return x+1; });
你可以看粗来,promise1.then()返回另一个promise,我把它命名为promise2。当promise1办完x的事情后,成功回调就会返回x+1。这时promise2就会代入x+1运行。
另一个简单的例子:
var promise2 = async().then(function(data) { console.log(data); ... // Do something with data // Returns nothing! });
这里,当从async()返回的promise解决后,成功回调就会触发,promise2就会代入undefined解析(成功回调没有返回值)。
你可以看粗来,promises可以带上参数,而且总是在回调函数出发后代入返回值运行。
为了演示一下,这里用promise做了个比较傻的例子(其实没必要用promises):
function async(value) { var deferred = $q.defer(); var asyncCalculation = value / 2; deferred.resolve(asyncCalculation); return deferred.promise; } var promise = async(8) .then(function(x) { return x+1; }) .then(function(x) { return x*2; }) .then(function(x) { return x-1; }); promise.then(function(x) { console.log(x); });
这个promises链开始的时候召唤async(8),async(8)运行后就给了promise一个4,4通过层层回调得到了9。((8 / 2 + 1) * 2 - 1
))。
如果我们用一个promise作为参数(不用变量)呢?假设我们有两个异步方法,async1()和async2(),每个都返回一个promise。看看:
var promise = async1() .then(function(data) { // Assume async2() needs the response of async1() in order to work var async2Promise = async2(data); return async2Promise; });
恩,这次的成功回调扮演另一个异步操作,同时返回一个promise。从async1().then()返回的是一个promise,现在可以通过async2Promse来解析运行。
async2()的参数是async1()运行后的值,async2()返回的是一个promise,我们也可以这样写:
var promise = async1() .then(async2);
恩,下一个例子(还是async1()和async2())。
// 我们意淫一下这是真的异步方法 function async1(value) { var deferred = $q.defer(); var asyncCalculation = value * 2; deferred.resolve(asyncCalculation); return deferred.promise; } function async2(value) { var deferred = $q.defer(); var asyncCalculation = value + 1; deferred.resolve(asyncCalculation); return deferred.promise; } var promise = async1(10) .then(function(x) { return async2(x); }); promise.then(function(x) { console.log(x); });
首先,我们调用async(10),经过运算得到20,然后返回一个promise。接着成功回调触发,async2(20)返回一个promise。最后输出21。同样功能却更易读的代码:
function logValue(value) { console.log(value); } async1(10) .then(async2) .then(logValue);
很容易看出我们先调用了async1(),然后我们调用了async2(),最后我们调用了logValue()。每个方法都用前一个方法resolved值作为参数。合理的命名也能帮助我们理解。
前面所有的例子都是理想化的,因为都是调用成功后的。但是以防一个promise因为什么失败了(rejected),绑定的promise也会失败(rejected)。
// Let's imagine those are really asynchronous functions function async1(value) { var deferred = $q.defer(); var asyncCalculation = value * 2; deferred.resolve(asyncCalculation); return deferred.promise; } function async2(value) { var deferred = $q.defer(); deferred.reject('rejected for demonstration!'); return deferred.promise; } var promise = async1(10) .then(function(x) { return async2(x); }); promise.then( function(x) { console.log(x); }, function(reason) { console.log('Error: ' + reason); });
这段代码的结果是Error: rejected for demonstration!。Promises可以绑定promise,根据绑定的promise来成功或者失败(通过绑定的promiser的resolve或者reject的值)。
一个例子:
async1() .then(async2) .then(async3) .catch(handleReject) .finally(freeResources);
在这个例子中,我们一个接一个的调用async1(),async2()和async3()。如果其中一个出现意外,成功调用链就会停止然后运行handleReject()。最后,无论成功与否都会触发freeResources()。比如说,async2()被rejected了,async3()就不会运行,
handleReject()会代入async2()被rejected的结果运行,最后触发freddResources()。
有用的方法
$q有几个帮助方法可以帮助我们使用promises。就像我说的,其他几种promises实现也有一样的方法,也许只是换了个名字。
有时候我们需要返回一个被拒绝的(rejected)promise。不用创建一个新的promise然后拒绝(rejected)它,我们可以使用$q.reject(reason)。$q.reject(reason)返回一个带有被拒绝原因的promise:
var promise = async().then(function(value) { if (isSatisfied(value)) { return value; } else { return $q.reject('value is not satisfied'); } }, function(reason) { if (canRecovered(reason)) { return newPromiseOrValue; } else { return $q.reject(reason); } });
如果async()通过一个合适的值接受了,这个值被绑定了,且会被promise接受。