AngularJs中$q服务(service)的使用

目录

前言

  $q服务的实现与ES6(ES2015)中的promise十分相似,这种方式改变了传统长期使用的JavaScript异步编程方式-处理回调函数。试想一下,如果有这样的业务逻辑:我们需要根据某个异步ajax请求的结果是否符合要求,再发起一个异步请求,在回调函数中对响应数据进行请求后再发起异步请求… …当出现这样的异步嵌套逻辑,代码结构就会变得很差。promise正是改变了这种情况,以一种流式的方式对异步请求进行声明和处理,表现出顺序的编码结构。本篇对$q服务的常用内容进行总结,而对于promise的学习,推荐阮一峰的ES6标准入门


综述

  $q是AngularJs中的一个服务,可以用来异步执行函数,并在异步请求执行完成时使用返回的结果。$q是对Kris Kowal提出的Kris Kowal’s Q中的promises/deferred对象的一种实现。$q有两种使用风格,一种和Kris Kowal’s Q或者jQuery中的defer实现类似,另一种实现与ES6(ES2015)中的promise十分相似。


$q构造函数

  上面提到的ES6流式风格的promise对象本质上就是把$q用作构造函数,并且这个构造函数的第一个参数是一个函数-resolver,构造函数返回一个promise对象。这个结果和原生的ES6的promise对象是十分类似的。
  要注意的是,虽然能够按照构造函数的形式来使用$q,但是并不是ES6的promise对象的所有方法在这里都得到了支持。
  根据AngularJs的官方文档,$q构造函数的典型使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// for the purpose of this example let's assume that variables `$q` and `okToGreet`
// are available in the current lexical scope (they could have been injected or passed in).

function asyncGreet(name) {
// perform some asynchronous operation, resolve or reject the promise when appropriate.
return $q(function(resolve, reject) {
setTimeout(function() {
if (okToGreet(name)) {
resolve('Hello, ' + name + '!');
} else {
reject('Greeting ' + name + ' is not allowed.');
}
}, 1000);
});
}

var promise = asyncGreet('Robin Hood');
promise.then(function(greeting) {
alert('Success: ' + greeting);
}, function(reason) {
alert('Failed: ' + reason);
});

上面这种写法,执行asyncGreet(‘Robin Hood’)返回了一个$q构造函数,该函数执行后要么调用resolve,要么调用reject,返回一个promise对象。之后再调用这个对象上的then()方法,具体下面有解释。


Deferred API

  一个新的deferred实例—延迟对象实例,是通过调用$q.defer()创建的。
  创建deferred对象的目的是:

  1. 暴露出一个与之相关的promise对象
  2. 提供标识任务状态以及是否成功完成的API接口

对应上述1,$q.defer()创建的延迟对象实例有以下属性:

  • promise

可通过下面方法调用:

1
2
var deferred = $q.defer();
var promise = deferred.promise;

对应上述2,$q.defer()创建的延迟对象实例有以下方法:

  • resolve(value)
      表明成功解决了派生的promise对象,此时value值将会作为参数值传递给对应promise对象的then()方法的第一个参数,而这个第一个参数是一个函数,即successCallback成功回调函数。也就是value值传递给successCallback函数。
      而如果这个value值是通过”$q.reject()”创建的rejection(实际上也是一个promise对象,详见下文),那么表明deferred的promise对象未被成功处理。
  • reject(reason)
      表明未能成功解决派生的promise对象,这和下文要提到的通过”$q.reject()”创建的rejection来处理promise是一样的。
  • notify(value)
      根据promise的执行状态来提供更新情况。在promise对象被resolve或者被reject前,这个方法可能会被调用多次。

Promise API

当一个deferred实例被成功创建时,随之而来的也成功创建了一个promise实例,并且可以通过调用deferred.promise来获取到这个promise实例。
  这个promise对象的目的是允许在延迟执行任务deferred执行完成时获取对其执行结果的访问权限。
Promise API提供如下3个方法:

  • then(successCallback, [errorCallback], [notifyCallback])
      无论是promise已经被resolve或者reject,还是promise将要被resolve或者reject,只要结果一旦可用,then()方法就会异步调用成功回调函数或者失败回调函数中的一个。回调函数有一个参数:成功执行的结果或者拒绝执行的原因。另外,在promise被resolve或者reject之前,notifyCallback可能不会被调用,也可能调用多次用以表明执行进度。
      这个方法会通过successCallback或者errorCallback的返回值来返回一个新的被resolve或者被reject的promise对象(不能通过notifyCallback方法)
  • catch(errorCallback)
      是promise.then(null, errorCallback)的简写形式。
  • finally(callback, notifyCallback)
      此方法允许观察处理一个promise对象是完成还是拒绝。但这样做不用修改最后的value值。 这可以用来做一些释放资源或者清理无用对象的工作,不管promise 被拒绝还是解决。

链式promises

  由于调用promise对象的then()方法会返回一个新派生出的promise对象,因此很容易创建一个链式的promise调用。如下所示:

1
2
3
4
5
promiseB = promiseA.then(function(result) {
return result + 1;
});

//一旦promiseA调用完成,就会执行promiseB,并且promiseB的值是promiseA返回的结果值加1。

我们可以创建任意长度的链式调用。并且由于一个promise对象的执行可能要受另一个promise对象的影响,这使得在链上任意一点暂停或者推迟promise的执行成为可能。这一点很有用,比如http响应拦截器,具体参考AngularJs中使用拦截器interceptor对全局HTTP错误进行统一处理


Kris Kowal’s Q和$q的区别

主要有两点不同:

  1. $q是与$rootScope.Scope结合在一起的,AngularJs中的作用域模型观察机制意味着可以更加快速地将resolution或者rejection传入模型,同时避免浏览器的重新渲染,而这往往会造成界面UI的闪烁,体验不好。
  2. Q比$q有更多特性,但代价是体积上更大。$q是轻量级的,即便简单,但也包含了常见异步任务所需要的全部重要功能。

使用格式

$q(resolver);

  • 参数
    值:resolver(类型:function(function, function))
    解释:参数是一个函数,这个函数是用来处理或者拒绝新创建的promise对象的。它有两个参数,分别是一个函数,第一个函数是用来执行处理promise对象,第二个用来拒绝promise对象。

  • 返回值
    新创建的promise对象。


提供的方法

  • defer();
    解释
    创建一个延迟对象deferred,它代表着一个在将来完成的任务。
    参数
    无。
    返回值
    一个新的deferred实例。

  • reject(reason);
    解释
      创建一个promise对象,这个对象的处理状态是被拒绝,参数reson表明具体原因。这个API应该用来在链式promise中向前传递rejection。如果是在处理链式调用中的最后一个promise对象,就不用担心这个了。
      当把deferreds/promises和有类似行为的try/catch/throw作比较时,应该这样想:把reject类比为JavaScript中的throw关键字。这就意味着,如果你通过promise的errorCallback捕获到了一个错误,并且你想将这个错误从当前promise对象向派生出的promise对象继续传播时,你就必须将这个错误重新抛出,而方法就是,使用该API,通过reject(reason);返回一个rejection。示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    promiseB = promiseA.then(function(result) {
    // success: do something and resolve promiseB
    // with the old or a new result
    return result;
    }, function(reason) {
    // error: handle the error if possible and
    // resolve promiseB with newPromiseOrValue,
    // otherwise forward the rejection to promiseB
    if (canHandle(reason)) {
    // handle the error and recover
    return newPromiseOrValue;
    }
    return $q.reject(reason);
    });

参数
reson(类型任意)。可以是代表着异常原因的常量、消息、对象等。
返回值
promise对象。并且这个对象已经被处理为rejected,参数就是reson。

  • when(value, [successCallback], [errorCallback], [progressCallback]);
    解释
    该方法会将一个普通任意值或者第三方的promise对象统一包装成$q的promise对象,第一个参数若不是promise对象则直接运行successCallback,若为promise那么返回的promise其实就是对这个promise类型的参数的一个包装,被传入的这个promise对应的defer发送的消息,会被我们when函数返回的promise对象所接收到。示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var promise = $q.when("a value", function(val) {
console.log("a value---resolve,this is " + val);
}, function() {
console.log("a value---reject");
});

var defer1 = $q.defer();
var promise1 = defer1.promise;
var promise2 = $q.when(promise1, function(val) {
console.log("a (3rd party) then-able promise,this is " + val);
}, function() {
console.log("a (3rd party) then-able promise---reject");
});
defer1.reject(1);

//运行结果:
//a value---resolve,this is a value
//a (3rd party) then-able promise---reject

参数
value:任意值或者promise对象
successCallback:可选,成功回调函数
errorCallback:可选,错误回调函数
progressCallback:可选,执行过程通知回调函数
返回值
传入值包装后的promise对象。

  • resolve(value, [successCallback], [errorCallback], [progressCallback]);
    这是when()方法的一个别名,是为了保持和ES6的命名一致性。

  • all(promises);
    解释
    这个方法用来将多个promise对象绑定到一个promise对象上,当且仅当输入的所有promise对象执行完成,这个对象才会被执行。
    参数
    promises:一个promise的数组或者hash表。
    返回值
    一个promise对象,这个对象传入的参数值是一个数组或者hash表,其中的每一个值和传入的promise数组一一对应相关,是相应promise执行的返回值。如果传入的promise数组中有任意一个得到了reject的结果,那么返回的这个promise也会因为相同的值而被reject。

  • race(promises);
    解释
    这个方法会传入promise数组或者对象,一旦传入的对象中有一个执行得到reject或者resolve的结果,那么就会返回,并且返回的promise的执行结果是一致的。
    参数
    promises:一个promise的数组或者promise对象。
    返回值
    一旦传入的对象中有一个执行得到reject或者resolve的结果,那么就会返回,并且返回的promise的执行结果是一致的。


使用案例一

  $q提供了很多方法和使用形式,但在实际中还没有用到太复杂的。下面这段代码是对$q的一个简单使用:列表的更新方法ctrl.update是一个异步操作,不知道确切在哪个时间点返回,那么这正符合$q的使用场景。
1.其中更新方法ctrl.update的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function(reqInfo,toTable) {
var params = {
'taskName':$scope.taskName,
'createUser':$scope.createUser,
'taskStatus':$scope.taskStatus
};
params = angular.extend(params,reqInfo);
$http.get("/rest/idap/taskList/index",{params:params}).success(function(result){
if(result.success){
toTable(result.data);
}
});
}

我们不需要管其中具体的业务逻辑,比如请求的参数、路由之类的。我们只需要知道,$http.get发起了一个异步请求,当成功执行时,我们会将返回的数据result.data传递给作为参数的函数toTable,由这个函数来将数据渲染到表格中。

2.接下来就是核心:$q如何使用。执行$q(function(resolve, reject) {})这部分后会返回一个新创建的promise对象,我们在function(resolve, reject) {}中发起数据查询请求:pageInfo只是一个配置表格分页信息的普通对象,不需多管。resolve是当promise对象成功执行时候用来进行处理的函数。我们就把这个函数传递给toTable方法,用来当查询成功时对表格进行数据渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$q(function(resolve, reject) {
if(!ctrl.removeLoadingBox){
loadingDialogId = Box.startLoading("查询中...");
}
ctrl.update({reqInfo:pageInfo,toTable:resolve});
}).then(function(msg) {
if(!ctrl.removeLoadingBox){
setTimeout(function(id){
return function() {
Box.stopLoading(id);
}
}(loadingDialogId),100);
}
if(angular.isObject(msg)) {
ctrl.config = msg;
} else {
Box.error(msg);
}
});

3.既然执行$q后返回promise对象,我们由上述promise API可知,通过then()方法的第一个参数可以指定promise成功执行后的回调函数,我们就在这里来具体指定我们在2中说的resolve到底是怎么处理的,是怎样一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function(msg) {
if(!ctrl.removeLoadingBox){
setTimeout(function(id){
return function() {
Box.stopLoading(id);
}
}(loadingDialogId),100);
}
if(angular.isObject(msg)) {
ctrl.config = msg;
} else {
Box.error(msg);
}
}

当promise成功执行,由1知道,传递给toTable()函数的参数是result.data,也就是说resolve()方法拿到的是result.data,也就是说successCallback-成功回调函数拿到的是result.data,那么上面的msg就是result.data,我们现在就可以拿着这个数据进行渲染了。这里我们只要把它赋值给ctrl.config即可(而config具体如何实现已经不是这里需要考虑的问题,总之它做的事情就是配置并渲染表格)。


使用案例二

  另一个案例就是使用$q异步服务对全局HTTP请求错误的进行拦截处理,具体内容参考这一篇AngularJs中使用拦截器interceptor对全局HTTP错误进行统一处理