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