“JavaScript Promises和AngularJS $q Service”Part 1 (基础篇)

来源:互联网 发布:嘉合信网络信息技术 编辑:程序博客网 时间:2024/05/16 11:06

注:本文是译文,难免有错误或理解不足之处,请大家多多指正,大家也可挪步原文。由于本文讲解十分精彩,非常推荐大家查看原文,由于原文内容十分丰富,所以将其分为2部分,这是Part 1(基础篇),戳这里查看Part 2(教程篇)。

promise或deferred在异步编程中简单而又实用。维基上列了一些promise模式的实现要点。AngularJS根据Kris Kowal’s Q 定义了了自己的实现方式。在本文中我将介绍promises和使用promises的目的,并且提供一个有关AngularJS $q Service的使用教程。

使用Promise (Deferred)的目的

JavaScript中使用回调函数来通知一个操作“成功”或“失败”的状态。例如,Geolocation api为了获取当前位置需要一个成功回调函数和一个失败回调函数:


Geolocation 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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

另一个常见的例子是XMLHttpRequest(用来进行ajax调用)。XMLHttpRequest对象的onreadystatechange回调函数会在其readyState属性值改变时被调用:


XHR使用回调函数:

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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

JavaScript异步编程中,类似的例子不胜枚举,但是需要同步使用多个异步操作时使用回调函数的方式就不合适了。

嵌套噩梦(依次执行)

假设我们有N个异步方法:async1(success, failure), async2(success, failure), …, asyncN(success, failure),现在我们想要他们依次执行,后一个需要在前一个方法的success回调中才能执行,每一个函数都有success回调和failure回调:

async1(function() {      async2(function() {        async3(function() {            async4(function() {                ....                    ....                        ....                           asyncN(null, null);                        ....                    ....                ....            }, null);        }, null);    }, null);}, null);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这样我们就遇到了著名的“嵌套噩梦”。即使上述代码有更好的表达方式,这样的代码也是难以阅读和维护的。

平行执行

假设我们有N个异步方法:async1(success, failure), async2(success, failure), …, asyncN(success, failure),并且我们想让他们平行执行,他们之间的执行是独立的,他们都执行完成后,我们弹出一条消息。每一个方法都有自己的success回调和failure回调:

var counter = N;function success() {      counter --;    if (counter === 0) {        alert('done!');    }}async1(success);  async2(success);  ........asyncN(success);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

我们首先声明了一个计数器,并将其初始值设为异步函数的个数,即N。当一个函数执行后,我们将计数器减1,并检测其是否是最后一个执行的函数。这种方式并不容易实现和维护,尤其是当每个函数都给success回调传参数。在这种情况下,我们需要保存函数每一次执行的结果。


在上面两个例子中,异步函数执行时,我们都必须指定success回调的处理方式。换句话说,当我们使用回调函数时,异步操作需要保留他们的引用,但是保留的引用可能并不属于我们的业务逻辑,这就提高了模块和service之间的耦合度,使代码的重用和测试变的复杂。

promise和deferred是什么?

deferred表示一次异步操作的结果,它对外的接口用于表示此次异步操作的状态和结果。通过它还能获得其对应的promise实例。

promise提供了与deferred通信的接口,从而外部能够通过promise得知deferred操作的状态和结果。

当deferred被创建时,其状态为“挂起(pending)”,并没有任何结果,当其被resolve()或reject()后,其状态变为“处理成功(resolved)”或“处理失败(rejected)”。我们甚至可以在deferred刚刚被创建后就可以获得其对应的promise实例,并且使用其完成某些功能。不过这些功能只有在deferred被resolve()或reject()后才能生效。

即使在我们还没想好要在deferred被resolve()或reject()之后需要做什么工作,我们也可以使用promise轻易创建一个异步操作。这就实现了低耦合。由于一个异步操作完成后不知道下一步应该做什么,所以它必须在完成后发出信号。

deferred可以改变一个异步操作的状态,而promise只能获取和查看这些状态,并不能改变状态。这就是为什么一个函数通常应该返回promise而不是deferred的原因,这样做使得外部的业务逻辑不能干涉异步操作的过程和状态。

在不同的编程语言(JavaScript, Java, C++, Python等)和框架(NodeJS, jQuery等)中对于promise的实现均不相同。AngularJS在$q Service的基础上实现promise。

怎样使用deferred和promise

通过上文了解了promise和deferred的含义和用途后,下面让我们来了解一下如何使用它们。如上文所说,promise的实现多种多样,不同的实现方式具有不同的用法,这部分内容会使用AngularJS的实现方式(自备梯子),即$q Service。如果你使用的其他的实现方式也不用担心,我在本文中提到的大部分方法都是通用的,如果不是,总有相同功能的方法。

基本用法

首先,让我们先创建一个deferred:

var myFirstDeferred = $q.defer();
  • 1
  • 1

再简单不过了,myFirstDeferred就是一个deferred,可以在异步操作结束后被resolve()或reject()。假设我们有个异步函数async(success, failure),参数为success回调和failure回调,当async函数执行完毕后,我们希望对myFirstDeferred进行resolve()或reject()操作:

async(function(value) {      myFirstDeferred.resolve(value);}, function(errorReason) {    myFirstDeferred.reject(errorReason);});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

由于AngularJS的$q Service不依赖于上下文的执行环境,上面的代码可以简写成:

async(myFirstDeferred.resolve, myFirstDeferred.reject);
  • 1
  • 1

得到myFirstDeferred的promise实例,并对其分配成功回调和失败回调是非常简单的:

var myFirstPromise = myFirstDeferred.promise;myFirstPromise      .then(function(data) {        console.log('My first promise succeeded', data);    }, function(error) {        console.log('My first promise failed', error);    });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

请注意,即使我们的异步函数async()还没有被执行,只要我们获得了deferred实例并得到其对应的promise,我们就可以对promise分配回调函数了:

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);    });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

如果async()执行成功了(resolve被执行),上面代码中的两个“成功回调”都会被执行,async()执行失败了(reject被执行),上面代码中的两个“失败回调”也都会被执行。

封装异步操作的一个好方法是定义一个返回promise的函数,这样调用者可以按需要分配成功或失败回调,而不能干涉或改变异步操作的状态:

function getData() {      var deferred = $q.defer();    async(deferred.resolve, deferred.reject);    return deferred.promise;}...... // Later, in a different filevar dataPromise = getData()  ......... // Much later, at the bottom of that file :)dataPromise      .then(function(data) {        console.log('Success!', data);    }, function(error) {        console.log('Failure...', error);    });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

直到现在,我们使用promise时还是分配了成功回调和失败回调,但其实也可以只分配成功回调或只分配失败回调:

promise.then(function() {      console.log('Assign only success callback to promise');});promise.catch(function() {      console.log('Assign only failure callback to promise');    // This is a shorthand for `promise.then(null, errorCallback)`});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

只传递成功回调给promise.then()就实现了“对promise只分配成功回调”,只传递失败回调给promise.catch()就实现了“对promise只分配失败回调”,而promise.catch()其实调用的是promise.then(null, errorCallback)。 
而如果我们想要在deferred被resolve()和reject()后都做某些工作呢?我们可以使用promise.finally()

promise.finally(function() {      console.log('Assign a function that will be invoked both upon success and failure');});
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

上述代码其实和下面的代码是等价的:

var callback = function() {      console.log('Assign a function that will be invoked both upon success and failure');};promise.then(callback, callback);
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

值和promise的链式操作

设想我们有个异步函数async()返回一个promise,有下面一段有趣的代码:

var promise1 = async();  var promise2 = promise1.then(function(x) {      return x+1;});
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

很容易理解,promise1.then()返回了另一个promise,这里命名为promise2,当promise1被处理(x作为参数传入),在promise1的成功回调中返回了x+1,这时promise2对应的处理函数将接收x+1作为参数。

再看一个类似的例子:

var promise2 = async().then(function(data) {      console.log(data);    ... // Do something with data    // Returns nothing!});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

async()函数返回的promise被处理,其成功回调函数没有返回任何值,那此时promise2对应的处理函数将接收到undefined。

上面可以看出,promise可以进行链式合成,并且上一个promise的处理结果将作为下一个promise的处理参数

为了演示效果,下面使用一个很傻的使用promise的例子(没有必要使用promise):

// Let's imagine this is really an asynchronous functionfunction 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);});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

这个promise链起始于async(8)的调用,async(8)返回的promise的成功回调的参数为4,这个参数4以及对其处理结果会在所有promise的成功回调中传递,所以最后打印出的结果将是(8/2+1)*2-1,即为9。

如果我们的链中传递的不是值,而是另一个promise,会发生什么呢?假设现在我们有2个异步回调函数: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;});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

不像上一个例子,这里async1()返回的promise的成功回调中执行了另一个异步操作并返回了一个promise:async2Promise。意料之中async1.then()返回的是一个promise,但是其结果要根据async2Promise的执行结果来看了,async2Promise可能执行成功回调,也可能执行失败回调。 
因为因为async2()的参数使用的是async1()函数处理的值,并且async2()也返回一个promise,那上面的代码可以简写成:

var promise = async1()      .then(async2);
  • 1
  • 2
  • 1
  • 2

下面是另一个例子,同样也仅是用作演示:

// Let's imagine those are really asynchronous functionsfunction 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);});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

首先我们调用了async1(10)async1函数对参数进行处理后(即resolve()操作)在其返回的promise的成功回调中传入的参数x为20,并执行了async2(20),而async2函数中同样对参数进行处理后返回promise,此时async2返回的promise成功回调中传入的参数将为21,所以最后打印的结果为21。

上述代码可以用下面可读性更强的表达方式:

function logValue(value) {      console.log(value);}async1(10)      .then(async2)    .then(logValue);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这样很容易看出执行的流程。

上面这些关于promise链的例子的结果是我们乐观处理的结果,即:我们假设promise执行的是都是成功的回调函数,即deferred都被resolve()了。但是如果deferred被reject()了,那整个promise链都将被rejected:

// Let's imagine those are really asynchronous functionsfunction 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); });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

很容易看出,最后打印的结果是Error: rejected for demonstration!,下面是一个关于promise链更高级的表示方法:

async1()      .then(async2)    .then(async3)    .catch(handleReject)    .finally(freeResources);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

这里,我们依次调用了async1()async2()async3()函数,如果其中某个函数被reject(),那么整个成功回调的链条将被打破,此时将执行handleReject()函数。而最后,不论怎样,freeResources()函数都会被执行。例如,如果async2()中被reject(),那么async3()将不会执行,handleReject()将接收async2()中reject()传入的参数(也可能不传参数)然后执行,最后执行freeResources()函数。

常用方法

AngularJS $q Service有一些非常有用的方法,这些方法在使用promise的时候会帮助很大。就像我开始所说的,其他的promise实现方式也有类似的方法,可能只是函数名不同。

有时我们需要返回一个被rejected的promise,我们可以使用$q.reject()返回一个带有参数的rejected 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);        }    });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

如果async()返回的promise的成功回调函数接收的参数(即deferred.resolve(value);中传递的参数)是合适的值(isSatisfied()函数返回true),那这个参数将被promise链接收并被resolve(),如果这个参数不是合适的值(isSatisfied()函数返回false),那$q.reject返回的rejected promise将被加入到promise链中,导致promise链被rejected。

如果async()返回的promise的失败回调函数接收的参数(即deferred.reject(param);中传递的参数)是合适的值(canRecovered()函数返回true),那么一个新值或promise将被加入到promise链中,如果这个参数不是合适的值(canRecovered()函数返回false),那$q.reject()返回的rejected promise将被加入到promise链中,导致promise链被rejected。

$q.reject()类似的是$q.when(),有时我们需要返回一个resolved promise,我们可以使用$q.when()返回一个带参数的resolved promise:

function getDataFromBackend(query) {      var data = searchInCache(query);    if (data) {        return $q.when(data);    } else {        return makeAsyncBackendCall(query);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

getDataFromBackend()函数用来从后台获取数据,不过在访问后台之前,先要在本地缓存中查找是否有相关的数据,如果有就使用$q.when()返回一个resolved promise。

$q.when()的功能不止于此,它还可以用来将第三方promise(如jQuery’s Deferred)封装成AngularJS对应的$q promise。

例如,jQuery的$.ajax()调用,返回的是jQuery的promise,可以使用如下方式转换成AngularJS的$q promise:

var jQueryPromise = $.ajax({      ...    ...    ...});var angularPromise = $q.when(jQueryPromise);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

有时候我们需要执行多个异步函数,不在意其执行顺序,只想在它们都执行完成后得到通知,可以使用$q.all(promiseArr)帮助我们实现这个功能。假设我们有N个异步方法:async1(), …, asyncN(),都返回promise,下面的代码只有当所有的操作都被resolved时才能打印出”done”:

var allPromise = $q.all([      async1(),    async2(),    ....    ....    asyncN()]);allPromise.then(function(values) {      var value1 = values[0],        value2 = values[1],        ....        ....        valueN = values[N];        console.log('done');});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

$q.all(promiseArr)当且仅当promiseArr数组里面所有的promise都被resolve时返回resoloved promise。注意,只要有一个promise被rejected,那得到的结果将是rejected promise。

到此为止,我们已经学习了怎样创建一个deferred,怎么对其进行resolve()和reject()操作,还学习怎样对其promise进行操作。我们还了解了一些AngularJS $q Service里的常用的方法,我想现在可以进行教程练习了。

请继续关注“JavaScript Promises和AngularJS $q Service” Part 2(教程篇)。

0 0
原创粉丝点击