angularJS $q and promise

网上一篇生动生动讲述了promise异步编程问题原文地址http://www.ngnice.com/posts/126ee9cf6ddb68

promise不是angular首创的,作为一种编程模式,它出现在……1976年,比js还要古老得多。promise全称是

Futures andpromises。具体的可以参见 http://en.wikipedia.org/wiki/Futures_and_promises 。而在javascript世界

中,一个广泛流行的库叫做Q 地址是https://github.com/kriskowal/q 而angular中的$q就是从它引入的。promise解决

的是异步编程的问题,对于生活在同步编程世界中的程序员来说,它可能比较难于理解,这也构成了angular入门门槛

之一,本文将用生活中的一个例子对此做一个形象的讲解。


假设有一个家具厂,而它有一个VIP客户张先生。

有一天张先生需要一个豪华衣柜,于是,他打电话给家具厂说我需要一个衣柜,回头做好了给我送来,这个操作

就叫$q.defer,也就是延期,因为这个衣柜不是现在要的,所以张先生这是在发起一个可延期的请求。同时,家具厂

给他留下了一个回执号,并对他说:我们做好了会给您送过去,放心吧。这叫做promise,也就是承诺。这样,这个

defer算是正式创建了,于是他把这件事记录在自己的日记上,并且同时记录了回执号,这叫做deferred,也就是已延

期事件。


现在,张先生就不用再去想着这件事了,该做什么做什么,这就是“异步”的含义。


假设家具厂在一周后做完了这个衣柜,并如约送到了张先生家(包邮哦,亲),这就叫做deferred.resolve(衣

柜),也就是“已解决”。而这时候张先生只要签收一下这个(衣柜)参数就行了,当然,这个“邮包”中也不一定只有衣

柜,还可以包含别的东西,比如厂家宣传资料、产品名录等。整个过程中轻松愉快,谁也没等谁,没有浪费任何时

间。

假设家具厂在评估后发现这个规格的衣柜我们做不了,那么它就需要deferred.reject(理由),也就是“拒绝”。拒绝

没有时间限制,可以发生在给出承诺之后的任何时候,甚至可能发生在快做完的时候。而且拒绝时候的参数也不仅仅

限于理由,还可以包含一个道歉信,违约金之类的,总之,你想给他什么就给他什么,如果你觉得不会惹恼客户,那

么不给也没关系。假设家具厂发现,自己正好有一个符合张先生要求的存货,它就可以用$q.when(现有衣柜)来把这

个承诺给张先生,这件事就立即被解决了,皆大欢喜,张先生可不在乎你是从头做的还是现有的成品,只会惊叹于你

们的效率之高。

假设这个家具厂对客户格外的细心,它还可能通过deferred.notify(进展情况)给张先生发送进展情况的“通知”。

这样,整个异步流程就圆满完成,无论成功或者失败,张先生都没有往里面投入任何额外的时间成本。


好,我们再扩展一下这个故事:

张先生这次需要做一个桌子,三把椅子,一张席梦思,但是他不希望今天收到个桌子,明天收到个椅子,后天又

得签收一次席梦思,而是希望家具厂做好了之后一次性送过来,但是他下单的时候又是分别下单的,那么他就可以重

新跟家具厂要一个包含上述三个承诺的新承诺,这就是$q.all(桌子承诺,椅子承诺,席梦思承诺),这样,他就不用再

关注以前的三个承诺了,直接等待这个新的承诺完成,到时候只要一次性签收了前面的这些承诺就行了。

ngularJS提供了一个内置Service $q,它提供了一种承诺/延后(promise/deferred),可以保证我们的调用代码

一定能够拿到数据。当然,我们可以猜到,最后去服务器取数据的方式肯定是异步的。只不过这个服务提供了表面上

是同步访问的API,当数据获取成功之后,自动将数据提供给调用的代码。

$q – A promise/deferred implementation inspired by Kris Kowal’s Q.The CommonJS Promise proposal

describes a promise as an interface for interacting with an object that representsthe result of an action that is

performed asynchronously,and may or may not be finished at any given point in time.

话不多说,上代码看看:

1. 创建一个Service,去服务器读取数据:

<span style="font-size:18px;"><span style="font-size:18px;">// $q 是内置服务,所以可以直接使用  
ngApp.factory('UserInfo',['$http','$q',function ($http,$q) {  
  return {  
    query : function() {  
      var deferred = $q.defer(); // 声明延后执行,表示要去监控后面的执行  
      $http({method: 'GET',url: 'scripts/mine.json'}).  
      success(function(data,status,headers,config) {  
        deferred.resolve(data);  // 声明执行成功,即http请求数据成功,可以返回数据了  
      }).  
      error(function(data,config) {  
        deferred.reject(data);   // 声明执行失败,即服务器返回错误  
      });  
      return deferred.promise;   // 返回承诺,这里并不是最终数据,而是访问最终数据的API  
    } // end query  
  };  
}]);  </span></span>

2. 在Controller上(以同步方式)使用这个Service:

<span style="font-size:18px;">angular.module('ngApp')  
  .controller('MainCtrl',['$scope','UserInfo',function ($scope,UserInfo) { // 引用我们定义的UserInfo服务  
    var promise = UserInfo.query(); // 同步调用,获得承诺接口  
    promise.then(function(data) {  // 调用承诺API获取数据 .resolve  
        $scope.user = data;  
    },function(data) {  // 处理错误 .reject  
        $scope.user = {error: '用户不存在!'};  
    });  
  }]);</span>


了解Promise

在谈论Promise之前我们要了解一下一些额外的知识;我们知道JavaScript语言的执行环境是“单线程”,所谓单

线程,就是一次只能够执行一个任务,如果有多个任务的话就要排队,前面一个任务完成后才可以继续下一个任务。

这种“单线程”的好处就是实现起来比较简单,容易操作;坏处就是容易造成阻塞,因为队列中如果有一个任务耗时比

较长,那么后面的任务都无法快速执行,或导致页面卡在某个状态上,给用户的体验很差。当然JavaScript提供了“异

步模式”去解决上述的问题,关于“异步模式”JavaScript提供了一些实现的方法。

  • 回调函数(callbacks)
  • 事件监听
  • Promise对象

关于回调函数,大家应该都不陌生,比如下面的代码(注:引用Leancloud上面的一点代码):

<span style="font-size:18px;">    AV.User.logIn("myname","mypass",{
            success: function(user) {
            // Do stuff after successful login.
            },error: function(user,error) {
            // The login failed. Check error to see why.
             }
    });</span>



用户通过用户名和密码来进行登录,如果登陆成功的话,会在success这个模块进行处理,如果登陆失败的话,
就会在error这个模块进行处理。
当我们需要处理的任务不是很多的情况下,使用回调函数还是可以应付的,也没有太大的问题,但是当我们需要

处理的任务比较多的时候,使用回调函数的弊端越来越明显了;首先,回调使得调用不一致,得不到保证;当依

赖于其它回调时,它们篡改代码的流程,是调试变得异常艰难,每一步调用之后都需要显式的处理错误;最后,过多

的回调使得代码的可读性和可维护性都变得很差,所以越来越多的程序员选择使用Promise去处理异步模式。

关于Promise我们会在下面进行详细的说明。


Promise是什么

Promise是一种异步方式处理值(或者非值)的方法,promise是对象,代表了一个函数最终可能的返回值或者

抛出的异常。

在与远程对象打交道时,Promise会非常有用,可以把它们看作远程对象的一个代理。
点击下面的链接可以查看Promise更多的信息

Promses/A+
Promises/A

使用Promise的理由

使用Promise可以让我们逃脱回调地狱,使我们的代码看起来像是同步的那样。

可以在程序中的任何位置捕捉错误,并且绕过依赖于程序异常的的后续代码,获得功能组合和错误冒泡的能力,最重要的是保持了异步运行的能力。

使我们的代码的可读性与可维护性都变得很好。

如何在AngularJS中使用Promise

要在AngularJS中使用Promise,要使用AngularJS的内置服务$q。

  • $q服务受到Kris Kowal的Q库的启发,所以类似于那个库,但是并没有包含那个库的所用功能。
  • $q是跟AngularJS的$rootScope模板集成的,所以在AngularJS中执行和拒绝都很快。
  • $q promise是跟AngularJS模板引擎集成的,这意味着在视图中找到任何Promise都会在视图中被执行或者拒绝。
  • 我们可以先使用$q的defer()方法创建一个deferred对象,然后通过deferred对象的promise属性,将这个对象变成一个promise对象;这个deferred对象还提供了三个方法,分别是resolve(),reject(),notify()。
下面我们来通过代码逐步地将上面的功能都实现,毕竟说得再多,不如你实实在在地把它们敲成代码去实现。


Test1
我们先通过一个同步的例子来创建一个promise对象。
HTML代码:

<span style="font-size:18px;"><div ng-app="MyApp">
    <div ng-controller="MyController">
        <label for="flag">成功
        <input id="flag" type="checkbox" ng-model="flag" /><br/>
        </label>
        <hr/>
        <button ng-click="handle()">点击我</button>
    </div>
</div></span>
JS代码:

<span style="font-size:18px;">angular.module("MyApp",[])
.controller("MyController",["$scope","$q",$q) {
            $scope.flag = true;
            $scope.handle = function () {
            var deferred = $q.defer();
            var promise = deferred.promise;

            promise.then(function (result) {
                alert("Success: " + result);
            },function (error) {
                alert("Fail: " + error);
            });

            if ($scope.flag) {
                deferred.resolve("you are lucky!");
            } else {
                deferred.reject("sorry,it lost!");
            }
        }
}]);</span>


我们来详细的分析一下上面的代码,我们在html页面上添加了一个checkbox,一个button目的是为了当我们选中checkbox和不选中checkbox时,点击下面的按钮会弹出不同的内容。

var deferred = $q.defer()这段代码创建了一个deferred对象,我们然后利用var promise = deferred.promise创建了一个promise对象。

我们给给promise的then方法传递了两个处理函数,分别处理当promise被执行的时候以及promise被拒绝的时候所要进行的操作。

下面的一个if(){}else{}语句块,包含执行和拒绝deferred promise,如果$scope.flag为true,那么我们就会执行deferred promise,然后我们给promise传递一个值,也可能是一个对象,表明promise执行的结果。如果$scope.flag为false,那么我们就会拒绝deferred promise,然后我们给promise传递一个值,也可能是一个对象,表明promise被拒绝的原因。

现在回过头来看看,promise的then方法,如果promise被执行,那么它的参数中的第一个函数的result就代表了"you are lucky!"

我们暂时用的是同步的模式,为的是能够说明问题,后面将会使用异步的方法。

到这里我们可以了解一下$q的defer()方法创建的对象具有哪些方法
  • resolve(value):用来执行deferred promise,value可以为字符串,对象等。
  • reject(value):用来拒绝deferred promise,value可以为字符串,对象等。
  • notify(value):获取deferred promise的执行状态,然后使用这个函数来传递它。
  • then(successFunc,errorFunc,notifyFunc):无论promise是成功了还是失败了,当结果可用之后,then都会立刻异步调用successFunc,或者'errorFunc',在promise被执行或者拒绝之前,notifyFunc可能会被调用0到多次,以提供过程状态的提示。
  • catch(errorFunc)
  • finally(callback)

使用then进行链式请求

我们通过使用then方法来进行链式调用,这样做的好处是,无论前一个任务或者说then函数是被执行或者拒绝了都不会影响后面的then函数的运行。
我们可以通过then创建一个执行链,它允许我们中断基于更多功能的应用流程,可以借此导向不同的的结果,这个中断可以让我们在执行链的任意时刻暂停后者推迟promise的执行。

Test2

HTML代码:

<span style="font-size:18px;"><div ng-app="MyApp">
    <div ng-controller="MyController">
        <label for="flag">成功
        <input id="flag" type="checkbox" ng-model="flag" /><br/>
        </label>
        <div ng-cloak>
            {{status}}
        </div>
        <hr/>
        <button ng-click="handle()">点击我</button>
    </div>
</div></span>

JS代码:

<span style="font-size:18px;">        angular.module("MyApp",[])
        .controller("MyController",$q) {
            $scope.flag = true;
            $scope.handle = function () {
            var deferred = $q.defer();
            var promise = deferred.promise;

            promise.then(function (result) {
                result = result + "you have passed the first then()";
                $scope.status = result;
                return result;
            },function (error) {
                error = error + "failed but you have passed the first then()";
                $scope.status = error;
                return error;
            }).then(function (result) {
                alert("Success: " + result);
            },function (error) {
                alert("Fail: " + error);
            })

            if ($scope.flag) {
                deferred.resolve("you are lucky!");
            } else {
                deferred.reject("sorry,it lost!");
            }
        }
}]);</span>


我们在Part1代码的基础上添加了一些代码,在原来的promise的链条上新添加了一个then()处理函数,目的就是

为了创建一个执行连,看看在这条执行连上,promise是如何被执行的。

需要注意的一点是,在第一个then()方法中,我们在第一个successFunc函数中将result的值进行了改变,在第二

个errorFunc函数中对error的值也进行了改变。

因为这个promise对象是贯穿整个执行链条的,所以在第一个then()方法中对其值进行改变必然会反映到后面的

then()方法中。

Test3
第三个例子,我们创建了一个服务,然后在这个服务中创建了一个promise,服务的目的就是为了拉取github上面关于angularjs一些pull的数据,详细的代码可以看下面

下面的例子包含的部分有点多,因为我是在以前的例子上做的改动,大家可以只看promise这部分。

目录结构:

MyApp

js

app.js

controller.js

service.js

views

home.html

index.html

js/app.js

angular.module("MyApp",["ngRoute","MyController","MyService"])
.config(["$routeProvider",function($routeProvider){
    $routeProvider
    .when('/',{
        templateUrl: "views/home.html",controller: "IndexController"
    });
}]);

js/controller.js

angular.module("MyController",[])
    .controller("IndexController","githubService",function($scope,githubService){
        $scope.name = "dreamapple";
        $scope.show = true;
        githubService.getPullRequests().then(function(result){
            $scope.data = result;
        },function(error){
            $scope.data = "error!";
        },function(progress){
            $scope.progress = progress;
            $scope.show = false;
        });
    }]);

js/servce.js

    angular.module("MyService",[])
    .factory('githubService',["$q","$http",function($q,$http){
        var getPullRequests = function(){
        var deferred = $q.defer();
        var promise = deferred.promise;
        var progress;
        $http.get("https://api.github.com/repos/angular/angular.js/pulls")
        .success(function(data){
            var result = [];
            for(var i = 0; i < data.length; i++){
                result.push(data[i].user);
                progress = (i+1)/data.length * 100;
                deferred.notify(progress);
            }
            deferred.resolve(result);
            })
        .error(function(error){
            deferred.reject(error);
        });
        return promise;
    }

    return {
        getPullRequests: getPullRequests
    };
}]);


views/home.html

<span style="font-size:18px;"><h1>{{name}}</h1>
<h2>Progress: {{progress}}</h2>
<h3 ng-show="show">Please wait a moment...</h3>
<p ng-repeat="person in data">{{person.login}}</p></span>

index.html

<!-- 不把下面的注释掉会出现问题,我是指上传到segmentfault上 -->
<!-- <head>
    <meta charset="UTF-8">
    <title>Route</title>
    <script src="http://cdn.bootcss.com/angular.js/1.4.0-rc.1/angular.js"></script>
    <script src="../node_modules/angular-route/angular-route.js"></script>
    <script src="js/app.js"></script>
    <script src="js/controller.js"></script>
    <script src="js/service.js"></script>
</head> -->
<body ng-app="MyApp">
    <header>
        <h1>Header</h1>
        <hr/>
    </header>
    <div ng-view>
    </div>
    <footer>
        <hr/>
        <h1>Footer</h1>
    </footer>
</body>

相关文章

ANGULAR.JS:NG-SELECTANDNG-OPTIONSPS:其实看英文文档比看中...
AngularJS中使用Chart.js制折线图与饼图实例  Chart.js 是...
IE浏览器兼容性后续前言 继续尝试解决IE浏览器兼容性问题,...
Angular实现下拉菜单多选写这篇文章时,引用文章地址如下:h...
在AngularJS应用中集成科大讯飞语音输入功能前言 根据项目...
Angular数据更新不及时问题探讨前言 在修复控制角标正确变...