1. 程式人生 > >給你一個承諾,玩轉angularjs的Promise

給你一個承諾,玩轉angularjs的Promise

在談論Promise之前我們要了解一下一些額外的知識;我們知道JavaScript語言的執行環境是“單執行緒”,所謂單執行緒,就是一次只能夠執行一個任務,如果有多個任務的話就要排隊,前面一個任務完成後才可以繼續下一個任務。

這種“單執行緒”的好處就是實現起來比較簡單,容易操作;壞處就是容易造成阻塞,因為佇列中如果有一個任務耗時比較長,那麼後面的任務都無法快速執行,或導致頁面卡在某個狀態上,給使用者的體驗很差。

當然JavaScript提供了“非同步模式”去解決上述的問題,關於“非同步模式”JavaScript提供了一些實現的方法。

  • 回撥函式(callbacks)
  • 事件監聽
  • Promise物件

關於回撥函式,大家應該都不陌生,比如下面的程式碼(注:引用Leancloud上面的一點程式碼):

    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.
             }
    });

使用者通過使用者名稱和密碼來進行登入,如果登陸成功的話,會在success這個模組進行處理,如果登陸失敗的話,就會在error這個模組進行處理。

當我們需要處理的任務不是很多的情況下,使用回撥函式還是可以應付的,也沒有太大的問題,但是當我們需要處理的任務比較多的時候,使用回撥函式的弊端越來越明顯了;首先,回撥使得呼叫不一致,得不到保證;當依賴於其它回撥時,它們篡改程式碼的流程,是除錯變得異常艱難,每一步呼叫之後都需要顯式的處理錯誤;最後,過多的回撥使得程式碼的可讀性和可維護性都變得很差,所以越來越多的程式設計師選擇使用Promise去處理非同步模式。

關於Promise我們會在下面進行詳細的說明。

Promise是什麼

Promise是一種非同步方式處理值(或者非值)的方法,promise是物件,代表了一個函式最終可能的返回值或者丟擲的異常。

在與遠端物件打交道時,Promise會非常有用,可以把它們看作遠端物件的一個代理。

點選下面的連結可以檢視Promise更多的資訊

使用Promise的理由

  • 使用Promise可以讓我們逃脫回撥地獄,使我們的程式碼看起來像是同步的那樣。
  • 可以在程式中的任何位置捕捉錯誤,並且繞過依賴於程式異常的的後續程式碼,獲得功能組合和錯誤冒泡的能力,最重要的是保持了非同步執行的能力。
  • 使我們的程式碼的可讀性與可維護性都變得很好。

如何在AngularJS中使用Promise

要在AngularJS中使用Promise,要使用AngularJS的內建服務$q

  • $q服務受到Kris KowalQ庫的啟發,所以類似於那個庫,但是並沒有包含那個庫的所用功能。
  • $q是跟AngularJS$rootScope模板整合的,所以在AngularJS中執行和拒絕都很快。
  • $q promise是跟AngularJS模板引擎整合的,這意味著在檢視中找到任何Promise都會在檢視中被執行或者拒絕。

我們可以先使用$qdefer()方法建立一個deferred物件,然後通過deferred物件的promise屬性,將這個物件變成一個promise物件;這個deferred物件還提供了三個方法,分別是resolve(),reject(),notify()

下面我們來通過程式碼逐步地將上面的功能都實現,畢竟說得再多,不如你實實在在地把它們敲成程式碼去實現。

Test1

我們先通過一個同步的例子來建立一個promise物件。

HTML程式碼:

<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>

JS程式碼:

angular.module("MyApp", [])
.controller("MyController", ["$scope", "$q", function ($scope, $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!");
            }
        }
}]);

我們來詳細的分析一下上面的程式碼,我們在html頁面上添加了一個checkbox,一個button目的是為了當我們選中checkbox和不選中checkbox時,點選下面的按鈕會彈出不同的內容。

var deferred = $q.defer()這段程式碼建立了一個deferred物件,我們然後利用var promise = deferred.promise建立了一個promise物件。

我們給給promisethen方法傳遞了兩個處理函式,分別處理當promise被執行的時候以及promise被拒絕的時候所要進行的操作。

下面的一個if(){}else{}語句塊,包含執行和拒絕deferred promise,如果$scope.flagtrue,那麼我們就會執行deferred promise,然後我們給promise傳遞一個值,也可能是一個物件,表明promise執行的結果。如果$scope.flagfalse,那麼我們就會拒絕deferred promise,然後我們給promise傳遞一個值,也可能是一個物件,表明promise被拒絕的原因。

現在回過頭來看看,promisethen方法,如果promise被執行,那麼它的引數中的第一個函式的result就代表了"you are lucky!"

我們暫時用的是同步的模式,為的是能夠說明問題,後面將會使用非同步的方法。

到這裡我們可以瞭解一下$qdefer()方法建立的物件具有哪些方法

  • resolve(value):用來執行deferred promisevalue可以為字串,物件等。
  • reject(value):用來拒絕deferred promisevalue可以為字串,物件等。
  • 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程式碼

<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>

JS程式碼:

        angular.module("MyApp", [])
        .controller("MyController", ["$scope", "$q", function ($scope, $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!");
            }
        }
}]);

我們在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", ["$scope", "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;
        });
    }]);

views/home.html

<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>

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>

關於$q還有一個方法,大家有興趣的話可以自己看看相關資料,我這裡就不多說了。。。

如果你覺得這篇文章哪裡說得不正確,歡迎大家指出來,一起進步!^_^