2014年3月22日 星期六

AngularJS $q deferred 和 promise

  剛接觸 AngularJS 時,每次看到 $q 和 promise,就開始亂用,久了之後唯一的想法就是:「QQ,不會用有再多保證也沒用阿!」。在耐心看完一些網路資料和書的範例後,才知道 $q 實在是一個簡單又好用的東西,只是不懂為什麼大部分的資料似乎都把它講的很玄...,因此決定在這邊記錄和分享一下。



$q 的故事

  與其很技術的說 $q 要如何使用,還不如說一個故事,如果大家能懂這個故事,基本上對 $q 就已經理解的差不多了。這個故事是這樣的...
你是個投資家,常決定要把錢投資給哪個新公司。有一天你決定要投資某個新公司,所以你到銀行去要領錢,結果銀行剛好進行大盤點,行員說要明天一早你才能領錢。可是你一定要在今天簽訂投資的合約,不然這個新公司就有別人要投資了!

行員:「公司引進了一個新系統 $q 可以解決這個問題!」

行員先在新系統 $q 登記了一個請款記錄 ($q.defer()),之後系統 $q 吐出一張智慧保證卡 ($q.defer().promise),行員把智慧保證卡給您。

行員:「先生,這是智慧保證卡,這張卡會在隔天票款好時會自動通知持有人,屆時拿著這張卡就可以當做現金了。」

於是你拿著智慧保證卡簽成了投資新公司的合約,並且把這張智慧保證卡給了公司的負責人。

隔天銀行完成了大盤點的工作,並在系統 $q 看到你的請款記錄,在準備好票款後,銀行用智慧保證卡通知了公司負責人($q.defer().resolve(投資款項));公司負責人在收到智慧保證卡的通知後,就開始安心的採購他所需要的用品了 ($q.defer().promise.then(投資款項))。
這個故事基本上在說的就是整個 $q 操作的方式與流程,簡單的整理如下。
銀行負責兩件事: (pseudo code)
1. 註冊請款:var cashCheckJob = $q.defer();
2. 準備好款項後,通知、給予款項: cashCheckJob.resolve(" Money is ready !");

你負責兩件事: (pseudo code)
1. 取得智慧保證卡:intelligentPromise = cashCheckJob.promise;
2. 智慧保證卡兌現後該做些什麼事: intelligentPromise.then(" Give company money. ");

有沒有覺得 $q 似乎沒這麼可怕了呢?現在我們可以來看看 code 了。

實際操作 $q

  在 angularJS 裡要使用 $q,需要將其注入(inject)進相關的 controller 或 Service 裡。底下我們用一個簡單的範例來說明如何使用。

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

myApp.controller('MyCtrl', function($scope, $q, $timeout){ // 注入 $q 這個 Service, $timeout 是為了模擬銀行處理程序而注入
    var bankCashCheck = function(cash){ // 銀行對領錢客戶的處理流程
        var cashCheckJob = $q.defer();  // 行員向系統 $q 註冊請款 job
        var promiseCard = cashCheckJob.promise; // 請款 job 回給行員一個智慧保證卡
        
       $timeout(function(){        // (Thread 2) 銀行進行的工作,這個動作是另一個 thread 處理了
            alert("Yes! We got it!");  // (Thread 2) 銀行盤點完
            cashCheckJob.resolve(cash);  // (Thread 2) 通知智慧保證卡並給予款項
        }, 1*1000);
        return promiseCard;  // 行員將智慧保證卡給你
    }

    $scope.getMoney = "N/A";
    $scope.cash = function(cash){  // 你去銀行領錢
        var promiseCard = bankCashCheck(cash); // 從行員手上拿到智慧保證卡
        promiseCard.then(function(getCash){ // 智慧保證卡兌現後得到 getCash 這麼多錢之後要做的事
            $scope.getMoney = getCash;
        });

        /** 通常在開發時,不會特別設一個參數而是直接寫成一行 
        bankCashCheck(100).then(function(getCash){
            $scope.getMoney = getCash;
        });
       **/
    };
});

  為了讓大家更有感覺,這邊有一個 jsfiddle 的 Demo

防錯功能

  話說銀行並非慈善機構,萬一有人來跟他領 $100 元,而實際上該帳戶只有 $50 元,銀行當然要拒絕這筆交易。$q 系統當然也設計了這個功能,只要觸發該筆交易的 reject 功能即可。這時候銀行負責的不只兩件事,而多了一件:(pseudo code)
1. 註冊請款:var cashCheckJob = $q.defer();
2. 確保帳戶有足夠金額,準備好款項後,通知、給予款項: cashCheckJob.resolve(" Money is ready !");
3. 帳戶金額不足,拒絕請款:cashCheckJob.reject(" Your Account has no enough money !");

  而當時拿著智慧保證卡保證拿到前後可以做的事,不就都不能做了嗎?對於交易被拒絕,我們也要有一些處理才行,所以你也多了一件事,不過這件事要對應智慧保證卡所給的通知結果: (pseudo code)
1. 取得智慧保證卡:intelligentPromise = cashCheckJob.promise;
2. 智慧保證卡兌現或沒兌現後該做些什麼事: intelligentPromise.then(" Give company money. ", "Check my account");


  我們來修改一下剛剛銀行的 code。

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

myApp.controller('MyCtrl', function($scope, $q, $timeout){
    var bankCashCheck = function(cash){ 
        var cashCheckJob = $q.defer();
        var promiseCard = cashCheckJob.promise;
        
       $timeout(function(){
            alert("Yes! We got it!");
            var accountMoney = 100;
            if(accountMoney >= cash){  // 檢查帳戶是否有足夠金額
              cashCheckJob.resolve(cash);  // 金額足夠:通知智慧保證卡並給予款項
            }else{  // 金額不足:通知智慧保證卡並給予款項
              cashCheckJob.reject('No enough money.');
            }
            
        }, 1*1000);
        return promiseCard;  // 行員將智慧保證卡給你
    }

    $scope.getMoney = "N/A";
    $scope.cash = function(cash){  // 你去銀行領錢
        var promiseCard = bankCashCheck(cash);
        promiseCard.then(function(getCash){ // 智慧保證卡兌現後得到 getCash 這麼多錢之後要做的事
            $scope.getMoney = getCash;
        }, function(cancelReason){
           // 得到交易取消的通知和原因,可作其他處理
           alert("Can't get money, because "+ cancelReason); // Can't get money, because No enough money.
        });
    };
});

連續的 Promise

  簽完約,投資的新公司從你那拿到智慧保證卡後,就立刻去買機器,公司負責人只能透過智慧保證卡付款給機器廠商。於是公司負責人用智慧保證卡給機器廠商一個承諾,保證在銀行兌現後,會付給廠商款項。之後廠商就可以把訂製的機器給他們。所以整個流程就變成:
1. 取得智慧保證卡:intelligentPromise = cashCheckJob.promise;
2. 公司負責人用智慧保證卡給廠商承諾 (prmise.then() 會回傳一個新的 promise): var companyPromise = intelligentPromise.then(" Give company money.", "Check my account");
3. 廠商拿到公司負責人的promise後要做的事: companyPromise.then("Give/Not Give company machine.");

  最後給大家一個完整的 DEMO:



總結:

  最後幫大家再整理一下 $q, deferred, promise 的使用方法:
1. var defer = $q.defer();  // 用 $q 產生一個 deffered
2. var result = defer.promise;  // 把保證傳出去
3. defer.resolve(data);  // 事件處理結束成功,defer 把資料傳出去
4. defer.reject(reason);  // 事件處理結束失敗,defer 把原因傳出去
5. result.then(success, error);  // defer 處理成功 call success, defer 處理失敗 call error

Note:
1. promise.then 本身會回傳一個 promise,因此promise可以延伸下去。
2. AngularJS $http service 也幾乎和 $q 的處理模式一樣。

以上,希望大家以後不會再一看到 $q 或 promise 就 QQ 了~