angular學習筆記(二十九)-$q服務

詩&遠方發表於2014-08-12

angular中的$q是用來處理非同步的(主要當然是http互動啦~).

$q採用的是promise式的非同步程式設計.什麼是promise非同步程式設計呢? 

非同步程式設計最重要的核心就是回撥,因為有回撥函式,所以才構成了非同步程式設計,而回撥有三個關鍵部分:

一是什麼時候執行回撥,二是執行什麼回撥,三是回撥執行的時候傳入什麼引數.

就以最常見的jquery Ajax舉例,傳送一個請求後:

什麼時候執行回撥: 請求成功(或者失敗)的時候

執行什麼回撥: 根據請求成功或者失敗,執行相應的回撥函式

回撥執行的時候傳入的什麼引數: 也就是後臺返回的資料

在過去大多數場景下,我們的非同步程式設計都是這樣的格式:

function a(callback1,callback2){
    var bool;
    var data;
    /*a函式要做的事情,做完後會判斷bool是true還是false,並且給data賦值*/;
    
    //a函式完事兒,根據a函式的執行結果執行相應的回撥函式 
    if(bool){
        callback1(data)     
    }
    if(!bool){
        callback2(data)
    } 
}

a(function(data){
    /*回撥函式1的處理*/
},function(data){
    /*回撥函式2的處理*/
}
)

執行: http://jsfiddle.net/s2ebjon0/

這個例子只有一次回撥,但如果回撥中還要巢狀回撥:

function a(callback1,callback2){
    var bool;
    var data;
    /*a函式要做的事情,做完後會判斷bool是true還是false,並且給data賦值;*/
    bool=true;
    data='code_bunny'
    
    //a函式完事兒,根據a函式的執行結果執行相應的回撥函式 
    if(bool){
        callback1(data,function(data){
            console.log('success:'+data)
        },function(data){
            console.log('fial:'+data)
        })     
    }
    if(!bool){
        callback2(data)
    } 
}

a(function(data,callback1,callback2){
    alert('成功'+data);
    var dataNew;
    var bool;
    dataNew = data;
    bool = false;
    if(bool){
        callback1(data)
    }
    if(!bool){
        callback2(data)
    }
    
},function(data){
    /*回撥函式2的處理*/
    alert('失敗'+data)
}
)

執行: http://jsfiddle.net/kbyy73dn/1/ 

 

我就不接著寫如果回撥中巢狀回撥再巢狀回撥再...

總之一句話,使用傳統的回撥函式作為引數來編寫方式來實現非同步,是十分麻煩的,程式碼可讀性十分的差.而promise式的程式設計則把這個過程抽象化,只關注上面說到的三個關鍵點(什麼時候執行回撥,執行什麼回撥,回撥執行的時候傳入什麼引數),在這篇文章不關心它究竟是如何做到的,只關心它是怎麼使用的:

promise式非同步有兩個重要的物件,一個defer物件,一個promise物件,每個defer物件都有和它繫結的promise物件,他們之間的關係是一一對應的.defer物件負責告知promise物件什麼時候執行回撥,執行什麼回撥,回撥執行的時候傳入什麼引數,而promise物件負責接收來自defer物件的通知,並且執行相應的回撥.

舉個最簡單的例子:

var HttpREST = angular.module('Async',[]);


HttpREST.controller('promise',function($q,$http){
//建立了一個defer物件;
var defer = $q.defer();
//建立了defer物件對應的promise
var promise = defer.promise;
//promise物件定義了成功回撥函式,失敗回撥函式
promise.then(function(data){console.log('成功'+data)},function(data){console.log('失敗'+data)});

//對promise發起通知: 1.執行這段程式碼的時候就是執行回撥的時候, 2.呼叫resolve方法,表示需要被執行的是成功的回撥, 3.resolve裡的引數就是回撥執行的時候需要被傳入的引數
defer.resolve(
'code_bunny') });

 

下面來看下$q的完整api

$q的方法:

一. $q.defer():

返回一個物件.一般把它賦值給defer變數:

var defer = $q.defer()

※defer的方法:

  (一)defer.resolve(data)

      對promise發起通知,通知執行成功的回撥,回撥執行的引數為data

  (二)defer.reject(data)

      對promise發起通知,通知執行失敗的回撥,回撥執行的引數為data

  (三)defer.notify(data)

      對promise發起通知,通知執行進度的回撥,回撥執行的引數為data

※defer的屬性:

  (一)defer.promise

  ※defer.promise的屬性:

    1.defer.promise.$$v

           promise的$$v物件就是對應的defer傳送的data,當defer還沒有傳送通知時,$$v為空.

           有一點很重要,假設,我們令$scope.a = defer.promise,那麼頁面在渲染{{a}}時,使用的是a.$$v來渲染a這個變數的.並且修改a變數,檢視不會發生變化,需要修改a.$$v,檢視才會被更新,具體請參考:

           http://www.cnblogs.com/liulangmao/p/3907307.html

  ※defer.promise的方法:

    1.defer.promise.then([success],[error],[notify]):

           .then方法接受三個引數,均為函式,函式在接受到defer傳送通知時被執行,函式中的引數均為defer傳送通知時傳入的data.

           [success]: 成功回撥,defer.resolve()時呼叫

    [error]: 失敗回撥,defer.reject()時呼叫

    [notify]: 進度回撥,defer.notify()時呼叫

           .then()方法返回一個promise物件,可以接續呼叫.then(),注意,無論.then()是呼叫的success函式,還是error函式,還是notify函式,傳送給下一個promise物件的通知一定是成功通知,而引數則是函式的返回值.也就是說,then()方法裡的函式被執行結束後,即為下一個promise傳送了成功通知,並且把返回值作為引數傳遞給回撥.

    eg1: (單次呼叫)

           html:          

<!DOCTYPE html>
<html ng-app = 'Async'>
<head>
  <title>19. $q非同步程式設計</title>
  <meta charset="utf-8">
  <script src="angular.js"></script>
  <script src="script.js"></script>
</head>
<body>
<div ng-controller = "promise">
  <h3>{{name}}</h3>
</div>
</body>
</html>

    js:

var HttpREST = angular.module('Async',[]);

//defer.resolve(),defer.reject(),defer.notify()
HttpREST.controller('promise',function($q,$http,$scope){
    var defer = $q.defer();         //建立了一個defer物件;

    var promise = defer.promise;    //建立了defer物件對應的promise

    promise.then(function(data){$scope.name='成功'+data},function(data){$scope.name='失敗'+data},function(data){$scope.name='進度'+data});

    $http({
        method:'GET',
        url:'/name'
    }).then(function(res){
        defer.resolve(res.data)
    },function(res){
        defer.reject(res.data)
    })
});

    如果正確建立後臺對於'/name'的請求處理,在一秒後返回'code_bunny',則一秒後頁面顯示:

    如果後臺沒有建立對於'/name'的請求處理,則頁面直接顯示:

           eg2: (鏈式呼叫)

    html:          

<!DOCTYPE html>
<html ng-app = 'Async'>
<head>
  <title>19. $q非同步程式設計</title>
  <meta charset="utf-8">
  <script src="angular.js"></script>
  <script src="script.js"></script>
</head>
<body>
<div ng-controller = "promise">
  <h3>{{name}}</h3>
  <h3>{{name2}}</h3>
</div>
</body>
</html>

      js:    

var HttpREST = angular.module('Async',[]);

//.then()的鏈式呼叫
HttpREST.controller('promise',function($q,$http,$scope){
    var defer = $q.defer();         //建立了一個defer物件;

    var promise = defer.promise;    //建立了defer物件對應的promise

    promise.then(function(data){
        $scope.name='成功'+data;
        return data+'2'
    },function(data){
        $scope.name='失敗'+data;
        return data+'2'
    },function(data){
        $scope.name='進度'+data;
        return data+'2'
    }).then(function(data){
        $scope.name2 = '成功'+data
    },function(data){
        $scope.name2 = '失敗'+data
    });

    $http({
        method:'GET',
        url:'/name'
    }).then(function(res){
        defer.resolve(res.data)
    },function(res){
        defer.reject(res.data)
    })
});

    如果正確建立後臺對於'/name'的請求處理,在一秒後返回'code_bunny',則一秒後頁面顯示:,可以看到,第一個.then()的成功的回撥返回的data+'2'這個值,被傳到了下一個.then()的成功回撥的data引數中

    如果後臺沒有建立對於'/name'的請求處理,則頁面直接顯示:,可以看到,就算第一個.then()呼叫的是失敗回撥,但是它發給下一個promise的通知依然是成功通知,data值就是失敗回撥的返回值

 

    2.defer.promise.catch([callback])          

    相當於.then(null,[callback])的簡寫. 直接傳入失敗回撥.返回一個promise物件.發給下一個promise物件的通知依然是成功通知.data值就是回撥的返回值.            

           *很早的angualr版本是沒有這個方法的. 

           eg:

    html:   

<!DOCTYPE html>
<html ng-app = 'Async'>
<head>
  <title>19. $q非同步程式設計</title>
  <meta charset="utf-8">
  <script src="angular.js"></script>
  <script src="script.js"></script>
</head>
<body>
<div ng-controller = "promise">
  <h3>{{name}}</h3>
  <h3>{{name2}}</h3>
</div>
</body>
</html>

    js:

//.catch()
HttpREST.controller('promise', function ($q, $http, $scope) {
    var defer = $q.defer();         //建立了一個defer物件;

    var promise = defer.promise;    //建立了defer物件對應的promise

    promise.catch(function (data) {
        $scope.name = data;
        return data+2
    }).then(function (data) {
            $scope.name2 = '成功' + data
        }, function (data) {
            $scope.name2 = '失敗' + data
        });

    $http({
        method: 'GET',
        url: '/name'
    }).then(function (res) {
            defer.resolve(res.data)
        }, function (res) {
            defer.reject(res.data)
        })
});

    後臺不建立'/name'的get請求響應, 得到的結果如下: 

           可以看到,promise物件收到通知,執行失敗回撥,然後返回新的promise,對新的promise來說,收到的通知還是執行成功回撥.回撥的引數是catch裡的函式的返回值.如果寫成:         

    promise.then(null,function (data) {
        $scope.name = data;
        return data+2
    }).then(function (data) {
            $scope.name2 = '成功' + data
        }, function (data) {
            $scope.name2 = '失敗' + data
        });

    兩者是完全一致的.

           注意,如果後臺正確建立了'/name'的get請求響應, 那麼,得到的結果會是:

           也就是說,catch()方法如果收到的通知不是執行失敗回撥,而是執行成功回撥,它直接返回一個promise物件進行鏈式呼叫,等於把成功通知傳給了下一個promise.

           由於catch([callback])方法得到的和.then(null,[callback])方法是完全一致的,程式碼上也米有精簡多少,所以一般就直接用.then就好了.

 

    3.defer.promise.finally([callback])

           .finally只接受一個回撥函式,而且這個回撥函式不接受引數.無論defer傳送的通知是成功,失敗,進度,這個函式都會被呼叫.

           .finally也返回一個promise物件,和上面兩個方法不同的是,它為下一個promise物件傳送的通知不一定是成功通知,而是傳給finally的通知型別.也就是說,如果defer給promise傳送的是失敗通知,那麼,finally()得到的promise它收到的也會是失敗通知,得到的引數也不是finally的返回值,而是第一個defer發出的通知所帶的data.

            *很早的angualr版本是沒有這個方法的. 

            eg:

     html:

<!DOCTYPE html>
<html ng-app = 'Async'>
<head>
  <title>19. $q非同步程式設計</title>
  <meta charset="utf-8">
  <script src="angular.js"></script>
  <script src="script.js"></script>
</head>
<body>
<div ng-controller = "promise">
  <h3>{{name}}</h3>
  <h3>{{name2}}</h3>
</div>
</body>
</html>

     js:

//.finally()
HttpREST.controller('promise', function ($q, $http, $scope) {
    var defer = $q.defer();         //建立了一個defer物件;

    var promise = defer.promise;    //建立了defer物件對應的promise

    promise.finally(function () {
        $scope.name = '已接收通知';
return 'code_dog';
}).then(function (data) { $scope.name2 = '成功' + data }, function (data) { $scope.name2 = '失敗' + data }); $http({ method: 'GET', url: '/name' }).then(function (res) { defer.resolve(res.data) }, function (res) { defer.reject(res.data) }) });

    後臺不建立'/name'的get請求響應, 得到的結果如下: 

    後臺正確建立'/name'的get請求響應, 得到結果如下:

    可以看到,當promise收到通知的時候執行了fianlly裡的回撥,然後返回的promise收到的通知和第一個promise收到的通知是一致的,不會受到finally中的回撥的任何影響.

           -------------------------------------------------------------------------------------------------------------------------------------------------------

           

二. $q.reject(data):

           這個方法(在我的認知範圍裡),就只能在promise的.then(funciton(){})函式裡面呼叫.作用是給.then()返回的下一個promise傳送錯誤資訊,並且給錯誤回撥傳入引數data

           eg1:(.then方法裡使用)

    html:

<!DOCTYPE html>
<html ng-app = 'Async'>
<head>
  <title>19. $q非同步程式設計</title>
  <meta charset="utf-8">
  <script src="angular.js"></script>
  <script src="script.js"></script>
</head>
<body>
<div ng-controller = "promise">
  <h3>{{name}}</h3>
  <h3>{{name2}}</h3>
</div>
</body>
</html>

    js:

HttpREST.controller('promise', function ($q, $http, $scope) {
    var defer = $q.defer();         //建立了一個defer物件;

    var promise = defer.promise;    //建立了defer物件對應的promise

    promise.then(function (data) {
        return $q.reject(data+'2')
    },function(){
        return $q.reject(data+'2')
    }).then(function (data) {
            $scope.name2 = '成功' + data
        }, function (data) {
            $scope.name2 = '失敗' + data
        });

    $http({
        method: 'GET',
        url: '/name'
    }).then(function (res) {
            defer.resolve(res.data)
        }, function (res) {
            defer.reject(res.data)
        })
});

    後臺正確建立'/name'的get請求響應時,得到的結果是:

    後臺沒有正確建立'/name'的get請求響應時,得到結果:

    可以看到,在then()方法的函式中,用$q.reject(data)來包裝返回值,可以給下一個返回的promise傳送失敗通知併傳送data引數.所以無論promise收到的是成功通知還是失敗通知,下一個promise收到的都是失敗通知.

 

    eg2:(.finally方法裡呼叫)

    html:

<!DOCTYPE html>
<html ng-app = 'Async'>
<head>
  <title>19. $q非同步程式設計</title>
  <meta charset="utf-8">
  <script src="angular.js"></script>
  <script src="script.js"></script>
</head>
<body>
<div ng-controller = "promise">
  <h3>{{name}}</h3>
  <h3>{{name2}}</h3>
</div>
</body>
</html>

    js:

HttpREST.controller('promise', function ($q, $http, $scope) {
    var defer = $q.defer();         //建立了一個defer物件;

    var promise = defer.promise;    //建立了defer物件對應的promise

    promise.finally(function () {
        return $q.reject('code_dog')
    }).then(function (data) {
            $scope.name2 = '成功' + data
        }, function (data) {
            $scope.name2 = '失敗' + data
        });

    $http({
        method: 'GET',
        url: '/name'
    }).then(function (res) {
            defer.resolve(res.data)
        }, function (res) {
            defer.reject(res.data)
        })
});

    無論後臺是否正確建立'/name'的get請求響應,得到結果都是:.因為.finally()的回撥是不能接受到data引數的.所以返回值都是一樣.

    上面已經說過,使用.finally方法的時候,回撥是不能接受引數的,回把對promise發的通知原封不動的(成功失敗,data)傳送給下一個promise物件.

    但是,如果我在.finally的回撥裡用$q.reject(data)來包裝了返回值,那麼傳送給下一個promise的通知會以$q.reject(data)為準,也就是'失敗通知',回撥引數為data.

        

三. $q.all([promise1,promise2,...]):

           $q.all接受一個陣列型別的引數,陣列的值為多個promise物件.它返回一個新的promise物件.

           當陣列中的每個單一promise物件都收到了成功通知,這個新的promise物件也收到成功通知(回撥引數是一個陣列,陣列中的各個值就是每個promise收到的data,注意順序不是按照單個promise被通知的順序,而是按照[promise1,promise2]這個陣列裡的順序)

           當陣列中的某個promise物件收到了失敗通知,這個新的promise物件也收到失敗通知,回撥引數就是單個promise收到的失敗通知的回撥引數

    eg:

           html:

<!DOCTYPE html>
<html ng-app = 'Async'>
<head>
  <title>19. $q非同步程式設計</title>
  <meta charset="utf-8">
  <script src="angular.js"></script>
  <script src="script.js"></script>
  <style type="text/css">
    h4 {
      color:red
    }
  </style>
</head>
<body>
<div ng-controller = "promise">
  <h3>{{name1}}</h3>
  <h3>{{name2}}</h3>
  <h3>{{age1}}</h3>
  <h3>{{age2}}</h3>
  <h4>{{three}}</h4>
</div>
</body>
</html>

    js:

//$q.all()
HttpREST.controller('promise', function ($q, $http, $scope) {
    var defer1 = $q.defer();         //建立了一個defer1物件;

    var promise1 = defer1.promise;    //建立了defer1物件對應的promise1

    var defer2 = $q.defer();         //再建立了一個defer2物件;

    var promise2 = defer2.promise;    //建立了新的defer2物件對應的promise2

    //promise1收到通知後執行的回撥:給name1和name2賦值
promise1.then(
function (data) { $scope.name1 = data; return data+'.2' },function(data){ $scope.name1 = data; return data+'.2' }).then(function (data) { $scope.name2 = 'promise1成功' + data }, function (data) { $scope.name2 = 'promise1失敗' + data });
//promise2收到通知後執行的回撥:給age1和age2賦值 promise2.then(
function (data) { $scope.age1 = data; return data+'.2' },function(data){ $scope.age1 = data; return data+'.2' }).then(function (data) { $scope.age2 = 'promise2成功' + data }, function (data) { $scope.age2 = 'promise2失敗' + data });
//建立一個promise3,它依賴於promise1和promise2
var promise3 = $q.all([promise1,promise2]); promise3.then(function(data){ $scope.three = data; },function(data){ $scope.three = data; }); $http({ method: 'GET', url: '/name' }).then(function (res) { defer1.resolve(res.data) }, function (res) { defer1.reject(res.data) }); $http({ method: 'GET', url: '/age' }).then(function (res) { defer2.resolve(res.data) }, function (res) { defer2.reject(res.data) }) });

    (1)後臺node正確建立兩個get請求的響應:

app.get('/name',function(req,res){
    setTimeout(function(){res.send('code_bunny')},2000)
});

app.get('/age',function(req,res){
    setTimeout(function(){res.send('18')},1000)
});

    一秒後顯示:

           兩秒後顯示:

    可以看到,當promise1和promise2都收到成功通知後,promise3也收到成功通知,而它的回撥的引數data就是一個陣列,陣列裡的兩個值分別是promise1收到的data和promise2收到的data,注意順序,這裡先收到通知的是promise2,但是promise3的data陣列裡值的順序和promise收到通知的順序無關.只和$q.all([])這個陣列裡的順序一致.

 

    (2)後臺node只建立了'/name'一個get請求的響應:

    

    顯示結果:

           可以看到,由於'/age'請求錯誤,promise2被通知失敗,所以promise3也立刻被通知失敗,收到的data引數也和promise2收到的data一致

 

四. $q.when(obj,[success],[error],[notify]):

           .when接受四個引數,其中,第二,第三,第四個引數都是函式,相當於promise.then()裡面的三個引數. 第一個引數有兩種可能:

           1. 第一個引數不是promise物件: 直接呼叫成功回撥,回撥的引數就是第一個引數本身

    eg:

    html:

 

//$q.when()
HttpREST.controller('promise', function ($q, $http, $scope) {

    $q.when('code_dog',function(data){
        $scope.name = data;
        return data+'2'
    },function(data){
        $scope.name = data;
        return data+'2'
    }).then(function (data) {
        $scope.name2 = '成功' + data
    }, function (data) {
        $scope.name2 = '失敗' + data
    });

});

 

    顯示結果:

    .when()的第一個引數不是promise物件,而是字串'code_dog',所以它直接執行成功回撥,也會返回下一個promise進行鏈式呼叫, 和一般的.then()是沒有區別的.

 

            在這種情況下其實不需要傳入第三,第四個引數,因為第一個引數如果不是promise,那麼它只會執行成功回撥.            

 

           2. 第一個引數是一個promise物件: 

               當這個promise物件收到通知的時候,呼叫回撥.回撥就是第二,三,四個引數...(相當於.then([success],[error],[notify]))

               另外,.when()返回的物件也就相當於.then()返回的物件.都是一個新的promise物件,都可以接收到回撥傳送的通知和引數...

               可以理解為,

               var defer = $q.defer(); 

               defer.promise.then([success],[error],[notify])

               這一段也可以寫成: 

               var defer = $q.defer();

               $q.when(defer.promise,[success],[error],[notify])

        eg:

        html:

<!DOCTYPE html>
<html ng-app = 'Async'>
<head>
  <title>19. $q非同步程式設計</title>
  <meta charset="utf-8">
  <script src="angular.js"></script>
  <script src="script.js"></script>
</head>
<body>
<div ng-controller = "promise">
  <h3>{{name}}</h3>
  <h3>{{name2}}</h3>
</div>
</body>
</html>

      js:

//$q.when()
HttpREST.controller('promise', function ($q, $http, $scope) {
    var defer = $q.defer();         //建立了一個defer物件;

    var promise = defer.promise;    //建立了defer物件對應的promise

    $q.when(promise,function(data){
        $scope.name = data;
        return data+'2'
    },function(data){
        $scope.name = data;
        return data+'2'
    }).then(function (data) {
        $scope.name2 = '成功' + data
    }, function (data) {
        $scope.name2 = '失敗' + data
    });
    
    /* 這樣寫得到的結果也是等價的.    
    promise.then(function(data){
        $scope.name = data;
        return data+'2'
    },function(data){
        $scope.name = data;
        return data+'2'        
    }).then(function (data) {
        $scope.name2 = '成功' + data
    }, function (data) {
        $scope.name2 = '失敗' + data
    });
    */

    $http({
        method: 'GET',
        url: '/name'
    }).then(function (res) {
        defer.resolve(res.data)
    }, function (res) {
        defer.reject(res.data)
    });

});

      和註釋掉的那一段寫法完全等價.

                 所以,在這種情況下.when()只是對.then()的一個包裝.

 

最後,api裡面說到:defer物件傳送訊息不會立即執行的,而是把要執行的程式碼放到了rootScope的evalAsync佇列當中,當時scope.$apply的時候才會被promise接收到這個訊息。

雖然不太明白這段話的意思,個人理解是大多數時候,scope是會自動$apply的...如果在什麼時候遇到promise沒有收到通知,那麼就試試看scope.$apply執行一下.

 

完整程式碼:https://github.com/OOP-Code-Bunny/angular/tree/master/OREILLY/19%20%24q%E5%92%8Cpromise 

 

相關文章