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

欢迎大家到我的博客关注我学习Ionic 1和Ionic 2的历程,共同学习,共同进步。

注:本文是译文,难免有错误或理解不足之处,请大家多多指正,大家也可挪步原文。由于本文讲解十分精彩,非常推荐大家查看原文,由于原文内容十分丰富,所以将其分为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);

另一个常见的例子是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();

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);

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

平行执行

假设我们有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);

我们首先声明了一个计数器,并将其初始值设为异步函数的个数,即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();

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

async(function(value) {  
    myFirstDeferred.resolve(value);
}, function(errorReason) {
    myFirstDeferred.reject(errorReason);
});

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

async(myFirstDeferred.resolve, myFirstDeferred.reject);

得到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);
    });

请注意,即使我们的异步函数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);
    });

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

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

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);
    });

直到现在,我们使用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)`
});

只传递成功回调给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');
});

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

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

值和promise的链式操作

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

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

很容易理解,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!
});

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

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

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

// Let's imagine this is really an asynchronous function
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);
});

这个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;
});

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

var promise = async1()  
    .then(async2);

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

// 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();
    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);
});

首先我们调用了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);

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

上面这些关于promise链的例子的结果是我们乐观处理的结果,即:我们假设promise执行的是都是成功的回调函数,即deferred都被resolve()了。但是如果deferred被reject()了,那整个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!,下面是一个关于promise链更高级的表示方法:

async1()  
    .then(async2)
    .then(async3)
    .catch(handleReject)
    .finally(freeResources);

这里,我们依次调用了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);
        }
    });

如果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);
    }
}

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);

有时候我们需要执行多个异步函数,不在意其执行顺序,只想在它们都执行完成后得到通知,可以使用$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');
});

$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(教程篇)

推荐一款个人使用了半年的理财产品:创建了6年的挖财,新人收益36%,7天18%,1年10%,注册送308元券

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值