angular中的$q與promise(綜合)

promise不是angular首創的,作爲一種編程模式,它出現在……1976年,比js還要古老得多。promise全稱是 Futures and promises。具體的可以參見 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(桌子承諾,椅子承諾,席夢思承諾),這樣,他就不用再關注以前的三個承諾了,直接等待這個新的承諾完成,到時候只要一次性簽收了前面的這些承諾就行了。

***********************************************************************************************************************************************

今天羣裏有位朋友問到直接返回$http說讀不到數據,原因在於$http是異步請求,而且是“不可期”的,你不知道什麼時候這個請求完成了。

而對於這種需要“同步”編程的方式,AngularJS提供了一個內置Service $q,它提供了一種承諾/延後(promise/deferred),可以保證我們的調用代碼一定能夠拿到數據。所以我們用起來可以像同步調用一樣,話說回來,最終還是xhr異步請求。

JS部分:

  1. app.factory('itemService', ['$http', '$q', function ($http, $q) {
  2. return {
  3. query : function() {
  4. var deferred = $q.defer();//聲明承諾
  5. $http({method: 'GET', url: '/item/list'}).
  6. success(function(data) {
  7. deferred.resolve(data);//請求成功
  8. }).
  9. error(function(data) {
  10. deferred.reject(data); //請求失敗
  11. });
  12. return deferred.promise; // 返回承諾,這裏返回的<strong><span style="color: #ff0000;">不是數據</span></strong>,而是API
  13. }
  14. };
  15. }]);
  1. angular.module('app')
  2. .controller('MainCtrl', ['itemService','$scope', function (itemService,$scope) { // 注入itemService
  3. var promise = itemService.query(); //獲得承諾接口
  4. promise.then(function(data) { // 成功回調
  5. $scope.user = data;
  6. }, function(data) { // 錯誤回調
  7. console.log('請求失敗');
  8. });
  9. }]);
*************************************************************************************************************************************************

一天早晨,爹對兒子說:“寶兒,出去看看天氣如何!”

每個星期天的早晨,爹都叫小寶拿着超級望遠鏡去家附近最高的山頭上看看天氣走勢如何,小寶說沒問題,我們可以認爲小寶在離開家的時候給了他爹一個promise。

這時候,他爹就想了,如果明天豔陽高照,他就準備去釣魚,如果天實在不行,就作罷,如果小寶對預報明天的天氣也沒底,他就在家宅一天哪也不去。

大概過了半小時,小寶回來了。每週的結果不盡相同:

A計劃 :天氣晴朗

小寶不辱使命,說外面陽光明媚,萬里無雲,這個promise was resolved(小寶信守承諾),爹就可以收拾行裝,釣魚去鳥。

B計劃: 小寶日觀天象,陰轉小雨的節奏

小寶依然不辱使命,但是天公不作美,promise was resolved,但是孩兒他爹覺得還是擱家待著吧。

C計劃:天象詭譎,小寶無法做出天氣走勢判斷

小寶敗興而歸,雲霧重重,遮蔽了視線,不敢妄言天氣走勢,小寶走的時候立下承諾說要給他爹預報天氣,但是沒有成功,我們說promise was rejected!孩兒他爹決定小心駛得萬年船,還是在家吧。

上述種種,用代碼寫出來是什麼樣子呢?

我們可以把孩兒他爹看成controller,小寶就是service。

整理邏輯:孩兒他爹讓小寶去看天氣,小寶不能立刻告訴他,但是孩兒他爹在等待結果的這段時間裏可以抽抽菸,喝喝茶啥的,因爲小寶承諾會把天氣情況搞清楚。等小寶把天氣預報帶回來,他就可以決定下一步幹啥。各位看官注意了:小寶登高望遠看天氣的時候並沒有影響他爹幹別的事情,這就是promise的妙處所在。

Angular裏有個then()函數,我們可以決定孩兒他爹到底是用哪個計劃(ABC)then()接收兩個functions作爲參數,第一個在promise is resolved的時候執行,另一個在promise is rejected的時候執行

Controller: FatherCtrl 

孩兒他爹掌控局面的代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// function somewhere in father-controller.js
       var makePromiseWithSon = function() {
           // This service's function returns a promise, but we'll deal with that shortly
           SonService.getWeather()
               // then() called when son gets back
               .then(function(data) {
                   // promise fulfilled
                   if (data.forecast==='good') {
                       prepareFishingTrip();
                   } else {
                       prepareSundayRoastDinner();
                   }
               }, function(error) {
                   // promise rejected, could log the error with: console.log('error', error);
                   prepareSundayRoastDinner();
               });
       };

Service: SonService

小寶的作用就是充當了一個service,他爬上山頭看天象,有點類似調用weather API而且還是異步調用,他得到的結果可能是個變量,也有可能出現異常情況(比如,返回500—>大霧瀰漫)。

從”Fishing Weather API”返回一個promise,如果it was resolved就格式化成{“forecase”:”good”}。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
app.factory('SonService', function ($http, $q) {
        return {
            getWeather: function() {
                // the $http API is based on the deferred/promise APIs exposed by the $q service
                // so it returns a promise for us by default
                return $http.get('http://fishing-weather-api.com/sunday/afternoon')
                    .then(function(response) {
                        if (typeof response.data === 'object') {
                            return response.data;
                        } else {
                            // invalid response
                            return $q.reject(response.data);
                        }
 
                    }, function(response) {
                        // something went wrong
                        return $q.reject(response.data);
                    });
            }
        };
    });

總結

這個比喻向我們展示了異步的實質,孩兒他爹不會倚門等待兒子的歸來,這段時間他完全可以自由活動。 孩兒他爹到底用哪個計劃取決於(天氣好/壞,沒有成功預報),小寶在臨走的時候給他爹一個promise,就等他回來的時候決定是resolve還是reject。

小寶其實是在使用異步服務(觀天象—調用weather API)來獲取天氣信息,孩兒他爹根本就不懂技術,坐等結果即可!

*************************************************************************************************************************************************

我們先來看一下傳統的javascript處理異步函數操作方式,這在jQuery中是很常見的:

$.get('api/user/42', function(userInfo) {
  console.log(userInfo); // or whatever
});

當發出一個get請求,獲取完用戶信息後,執行一個回調函數,並且將用戶的信息打印出來。非常簡潔漂亮,但是一旦出現了嵌套的回調函數,代碼就會變成這樣,可讀性非常差。

User.get(fromId, {
    success: function (err, user) {
        if (err) return {error: err};
        user.friends.find(toId, function (err, friend) {
            if (err) return {error: err};
            user.sendMessage(friend, message, callback);
        });
    },
    failure: function (err) {
        return {error: err}
    }
});

promises

promise的出現就是爲了解決異步操作中的“回調金字塔”。  CommonJS對 promise 進行了定義。並且列出了幾種實現。

a promise as an interface for interacting with an object that represents the result of an action that is performed asynchronously, and may or may not be finished at any given point in time.

promise的中文意思是承諾。指將來會在某個時間點發生的事情,這個事情是異步的,並且有可能成功,也有可能失敗。promise 就是對這個“承諾”的抽象。

現在改寫上面的代碼,一個面向promise的代碼應該是這樣的:

User.get(fromId)
    .then(function (user) {
        return user.friends.find(toId);
    }, function (err) {
        // We couldn't find the user
    })
    .then(function (friend) {
        return user.sendMessage(friend, message);
    }, function (err) {
        // The user's friend resulted in an error
    })
    .then(function (success) {
        // user was sent the message
    }, function (err) {
        // An error occurred
    });

這裏的代碼可讀性就比上面的好多了。get方法返回一個promise對象,then方法接收兩個參數:一個是異步操作成功的回調函數,和一個失敗的回調函數,返回另一個promise對象。

  • then(successCallback, errorCallback)

promise最重要的then的方法。then調用後返回又是一個新的promise對象,很容易能實現鏈式調用來解決異步操作中的“回調金字塔”

雖然CommonJS對其進行了規範,但是在各種實現的api還是略有不同,這裏主要來談一談angular中的$q

$q

具體的api 請點擊

Deferred api

想要創建promise就需要依賴於$q,首先需要通過$q.defer來創建出一個Deferred,Deferred對象用於定義一些異步操作成功後或失敗後的信息。

  • resolve(value):定義調用成功後的返回信息。
  • reject(reason):定義調用失敗後的返回信息。
  • notify(value):更新promise狀態,通常用於更新異步函數的操作進度。

promise api

  • then(successCallback, errorCallback, notifyCallback):定義一個promise完成之後的回調,返回一個新的promise。successCallback指promise成功後的回調,errorCallback指promise失敗後的回調,notifyCallback指更新promise的狀態。也可以只定義successCallback或定義successCallback, errorCallback。
  • catch(errorCallback):等同於promise.then(null, errorCallback)
  • finally(callback):失敗或成功後都執行的回調函數。

實踐

//userService
app.service('userService', function ($q) {
    var getUser = function (userId) {
        var defer = $q.defer();
        if (userId != 1) {
            defer.reject('not found user')
        } else {
            defer.resolve({
                name: "foo",
                age: '14',
                userId: userId,
                schoolId: 6
            })
        }
        return  defer.promise;
    };
    var getSchool = function (schoolId) {
        var defer = $q.defer();
        defer.resolve({
            schoolName: "nice school",
            schoolId: schoolId
        })
        return defer.promise;
    }
    return{
        getUser: getUser,
        getSchool: getSchool
    };
});
//Controller
app.controller('ctrl', function ($scope, userService) {
$scope.name = 'hello';
$scope.userId=1;
userService.getUser($scope.userId)
    .then(function (user) {
        $scope.user = user;
        return user;
    }, function (error) {
        $scope.error = error;
    })
    .then(function (userInfo) {
        $scope.school = userService.getSchool(userInfo.schoolId);
    })
});
//html
<body ng-app="app">
<div ng-controller="ctrl">
    <ul>
        <li>userId:{{userId}}</li>
        <li>name:{{name}}</li>
        <li>user:{{user}}</li>
        <li>error:{{error}}</li>
        <li>school:{{school}}</li>
    </ul>
</div>
//outPut
userId:1
name:hello
user:{"name":"foo","age":"14","userId":1,"schoolId":6}
error:
school:{"schoolName":"nice school","schoolId":6}

userService中,暴露出getUsergetSchool兩個方法。要得到school就必須得到user,要得到user必須輸入userId,只有當userId爲1時調用才成功。

userService

  • var defer = $q.defer();:定義Deferred對象。
  • defer.resolve(...):定義getUser調用成功後返回的信息。
  • defer.reject('not found user'):定義調用失敗後返回的信息。
  • 最後返回defer.promise promise對象。

Controller

  • app.controller('ctrl', function ($scope, userService):在Controller中注入userService
  • userService.getUser($scope.userId):調用getuser方法,返回的是一個promise對象。我們希望在獲取user對象後再調用getSchool方法。所以調用promise的then,方法,兩個參數,第一個參數定義調用成功的回調函數,第二個參數定義調用失敗的函數。then返回的又是一個promise,所以可以進行鏈式的調用then。第二個then,只寫了一個調用成功後回調函數,這個回調函數的參數來自於上一個then中成功返回的回調函數,這裏就是指user對象。
  • 如果userId爲非1則會有以下的輸出:

    userId:43
    name:hello
    user:
    error:not found user
    school:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章