AngularJS 中的 Promise 和 設計模式

來源:互聯網
上載者:User

標籤:des   blog   http   java   使用   os   

Promises And Design Patterns

寫得好長好長好長長~

解決 Javascript 非同步事件的傳統方式是回呼函數;調用一個方法,然後給它一個函數引用,當這個方法完結的時候執行這個函數引用。

$.get(‘api/gizmo/42‘, function(gizmo) {  console.log(gizmo); // or whatever});

看起來很不錯對不對,不過,也有缺點的;首先,合并或者連結多個非同步過程超複雜;要麼就是大量的模板代碼,要麼就是嗯哼你懂的回調地獄(一層套一層的回調):

$.get(‘api/gizmo/42‘, function(gizmo) {  $.get(‘api/foobars/‘ + gizmo, function(foobar) {    $.get(‘api/barbaz/‘ + foobar, function(bazbar) {      doSomethingWith(gizmo, foobar, bazbar);    }, errorCallback);  }, errorCallback);}, errorCallback);

明白了吧。其實在 Javascript 中,有另外一種非同步處理模式:更屌,在 Javascript 裡面經常被叫做 Promises, CommonJS 標準委員會於是發布了一個規範,就把這個 API 叫做 Promises 了。

Promise 背後的概念非常簡單,有兩部分:

  • Deferreds,定義工作單元
  • Promises,從 Deferreds 返回的資料

基本上,你會用 Deferred 作為通訊對象,用來定義工作單元的開始,處理和結束三部分。

Promise 是 Deferred 響應資料的輸出;它有狀態 (等待,執行和拒絕),以及控制代碼,或叫做回呼函數,反正就是那些在 Promise 執行,拒絕或者提示進程中會被調用的方法。

Promise 不同於回調的很重要的一個點是,你可以在 Promise 狀態變成執行(resolved)追加處理控制代碼。這就允許你傳輸資料,而忽略它是否已經被應用擷取,然後緩衝它,等等之類的操作,因此你可以對資料執行操作,而不管它是否已經或者即將可用。

在之後的文章中,我們將會基於 AngularJS 來講解 Promises 。AngularJS 的整個程式碼程式庫很大程度上依賴於 Promise,包括架構以及你用它編寫的應用代碼。AngularJS 用的是它自己的 Promises 實現, $q 服務,又一個 Q 庫的輕量實現。

$q 實現了上面提到的所有 Deferred / Promise 方法,除此之外 $q 還有自己的實現: $q.defer(),用來建立一個新的 Deferred 對象; $q.all(),允許等待多 Promises 執行終了,還有方法 $q.when()$q.reject(),具體我們之後會講到。

$q.defer() 返回一個 Deferred 對象,帶有方法 resolve(), reject(), 和 notify()。Deferred 還有一個 promise 屬性,這是一個 promise對象,可以用於應用內部傳遞。

promise 對象有另外三個方法: .then(),是唯一 Promise 規範要求的方法,用三個回調方法作為參數;一個成功回調,一個失敗回調,還有一個狀態變化回調。

$q 在 Promise 規範之上還添加了兩個方法: catch(),可以用於定義一個通用方法,它會在 promise 鏈中有某個 promise 處理失敗時被調用。還有 finally(),不管 promise 執行是成功或者失敗都會執行。注意,這些不應該和 Javascript 的異常處理混淆或者並用: 在 promise 內部拋出的異常,不會被 catch() 俘獲。(※貌似這裡我理解錯了)

Promise 簡單例子

下面是使用 $qDeferred,和 Promise 放一起的簡單例子。首先我要聲明,本文中所有例子的代碼都沒有經過測試;而且也沒有正確的引用 Angular 服務和依賴,之類的。不過我覺得對於啟發你怎麼玩,已經夠好了。

首先,我們先建立一個新的工作單元,通過 Deferred 對象,用 $q.defer():

var deferred = $q.defer();

然後,我們從 Deferred 拿到 promise,給它追加一些行為。

var promise = deferred.promise;promise.then(function success(data) {  console.log(data);}, function error(msg) {  console.error(msg);});

最後,我們假裝做點啥,然後告訴 deferred 我們已經完成了:

deferred.resolve(‘all done!‘);

當然,這不需要真的非同步,所以我們可以用 Angular 的 $timeout 服務(或者 Javascript 的 setTimeout,不過,在 Angular 應用中最好用 $timeout,這樣你可以 mock/test 它)來假裝一下。

$timeout(function() {  deferred.resolve(‘All done... eventually‘);}, 1000);

好了,有趣的是:我們可以追加很多個 then() 到一個 promise 上,以及我們可以在 promise 被 resolved 之後追加 then():

var deferred = $q.defer();var promise = deferred.promise;// assign behavior before resolvingpromise.then(function (data) {  console.log(‘before:‘, data);});deferred.resolve(‘Oh look we\‘re done already.‘)// assign behavior after resolvingpromise.then(function (data) {  console.log(‘after:‘, data);});

那,要是發生異常怎麼辦?我們用 deferred.reject(),它會出發 then() 的第二個函數,就像回調一樣。

var deferred = $q.defer();var promise = deferred.promise;promise.then(function success(data) {  console.log(‘Success!‘, data);}, function error(msg) {  console.error(‘Failure!‘, msg);});deferred.reject(‘We failed :(‘);

不用 then() 的第二個參數,還有另外一種選擇,你可以用鏈式的 catch(),在 promise 鏈中發生異常的時候它會被調用(可能在很多鏈之後)。

promise  .then(function success(data) {    console.log(data);  })  .catch(function error(msg) {    console.error(msg);  });

作為一個附加,對於長耗時的處理(比如上傳,長計算,批處理,等等),你可以用 deferred.notify() 作為 then() 第三個參數,給 promise 一個監聽來更新狀態。

var deferred = $q.defer();var promise = deferred.promise;promise  .then(function success(data) {    console.log(data);  },  function error(error) {    console.error(error);  },  function notification(notification) {    console.info(notification);  })); var progress = 0; var interval = $interval(function() {  if (progress >= 100) {    $interval.cancel(interval);    deferred.resolve(‘All done!‘);  }  progress += 10;  deferred.notify(progress + ‘%...‘); }, 100)
鏈式 Promise

之前我們已經看過了,你可以給一個 promise 追加多個處理(then())。Promise API 好玩的地方在於允許鏈式處理:

promise  .then(doSomething)  .then(doSomethingElse)  .then(doSomethingMore)  .catch(logError);

舉個簡單的例子,這允許你把你的函數調用切分成單純的,單一目的方法,而不是一攬子麻團;還有另外一個好處是你可以在多 promise 任務中重用這些方法,就像你執行鏈式方法一樣(比如說工作清單之類的)。

如果你用前一個非同步執行結果出發下一個非同步處理,那就更牛X了。預設的,一個鏈式,像上面示範的那種,是會把前一個執行結果對象傳遞給下一個 then() 的。比如:

var deferred = $q.defer();var promise = deferred.promise;promise  .then(function(val) {    console.log(val);    return ‘B‘;  })  .then(function(val) {    console.log(val);    return ‘C‘  })  .then(function(val) {    console.log(val);   });deferred.resolve(‘A‘);

這會在控制台輸出以下結果:

ABC

雖然例子簡單,但是你有沒有體會到如果 then() 返回另一個 promise 那種強大。這種情況下,下一個 then() 會在 promise 完結的時候被執行。這種模式可以用到把 HTTP 要求串上面,比如說(當一個請求依賴於前一個請求的結果的時候):

var deferred = $q.defer();var promise = deferred.promise;// resolve it after a second$timeout(function() {  deferred.resolve(‘foo‘);}, 1000);promise  .then(function(one) {    console.log(‘Promise one resolved with ‘, one);    var anotherDeferred = $q.defer();    // resolve after another second    $timeout(function() {      anotherDeferred.resolve(‘bar‘);    }, 1000);    return anotherDeferred.promise;  })  .then(function(two) {    console.log(‘Promise two resolved with ‘, two);  });

總結:

  • Promise 鏈會把上一個 then 的返回結果傳遞給調用鏈的下一個 then (如果沒有就是 undefined)

  • 如果 then 回掉返回一個 promise 對象,下一個 then 只會在這個 promise 被處理結束的時候調用。

  • 在鏈最後的 catch 為整個鏈式處理提供一個異常處理點

  • 在鏈最後的 finally 總是會被執行,不管 promise 被處理或者被拒絕,起清理作用

    Parallel Promises And ‘Promise-Ifying‘ Plain Values

我還提到了 $q.all(),允許你等待並行的 promise 處理,當所有的 promise 都被處理結束之後,調用共同的回調。在 Angular 中,這個方法有兩種調用方式: 以 Array 方式或 Object 方式。Array 方式接收多個 promise ,然後在調用 .then() 的時候使用一個資料結果對象,在結果對象裡麵包含了所有的 promise 結果,按照輸入數組的順序排列:

$q.all([promiseOne, promiseTwo, promiseThree])  .then(function(results) {    console.log(results[0], results[1], results[2]);  });

第二種方式是接收一個 promise 集合對象,允許你給每個 promise 一個別名,在回呼函數中可以使用它們(有更好的可讀性):

$q.all({ first: promiseOne, second: promiseTwo, third: promiseThree })  .then(function(results) {    console.log(results.first, results.second, results.third);  });

我建議使用數組標記法,如果你只是希望可以批處理結果,就是說,如果你把所有的結果都平等處理。而以對象方式來處理,則更適合需要自注釋代碼的時候。

另一個有用的方法是 $q.when(),如果你想通過一個普通變數建立一個 promise ,或者你不清楚你要處理的對象是不是 promise 時非常有用。

$q.when(‘foo‘)  .then(function(bar) {    console.log(bar);  });$q.when(aPromise)  .then(function(baz) {    console.log(baz);  });$q.when(valueOrPromise)  .then(function(boz) {    // well you get the idea.  })

$q.when() 在諸如服務中的緩衝這種情況也很好用:

angular.module(‘myApp‘).service(‘MyService‘, function($q, MyResource) {  var cachedSomething;  this.getSomething = function() {    if (cachedSomething) {      return $q.when(cachedSomething);    }    // on first call, return the result of MyResource.get()    // note that ‘then()‘ is chainable / returns a promise,    // so we can return that instead of a separate promise object    return MyResource.get().$promise      .then(function(something) {        cachedSomething = something      });  };});

然後可以這樣調用它:

MyService.getSomething()    .then(function(something) {        console.log(something);    });
AngularJS 中的實際應用

在 Angular 的 I/O 中,大多數會返回 promise 或者 promise-compatible(then-able)對象,但是,都挺奇怪的。$http 文檔 說,它會返回一個 HttpPromise 對象,嗯,確實是 promise,但是有兩個額外的(有用的)方法,應該不會嚇到 jQuery 使用者。它定義了 success()error() ,用來分別對應 then() 的第一和第二個參數。

Angular 的 $resource 服務,用於 REST-endpoints 的 $http 封裝,同樣有點奇怪;通用方法(get(),save()之類的四個)接收第二和第三個參數作為 successerror 回調,同時它們還返回一個對象,當請求被處理之後,會往其中填充請求的資料。它不會直接返回 promise 對象;相反,通過 get() 方法返回的對象有一個屬性 $promise,用來暴露 promise 對象。

一方面,這和 $http 不符,並且 Angular 的所有東西都是/應該是 promise,不過另一方面,它允許開發人員簡單的把 $resource.get() 的結果指派給 $scope。原先,開發人員可以給 $scope 指定任何 promise,但是從 Angular 1.2 開始被定義為過時了:請看this commit where it was deprecated。

我個人來說,我更喜歡統一的 API,所以我把所有的 I/O 操作都封裝到了 Service 中,統一返回一個 promise 對象,不過調用 $resource 有點糙。下面是個例子:

angular.module(‘fooApp‘)  .service(‘BarResource‘, function ($resource) {    return $resource(‘api/bar/:id‘);  })  .service(‘BarService‘, function (BarResource) {    this.getBar = function (id) {      return BarResource.get({        id: id      }).$promise;    }  });

這個例子有點晦澀,因為傳遞 id 參數給 BarResource 看起來有點多餘,不過它也還是有道理的,比如你有一個複雜的對象,但只需要用它的 ID 屬性來調用一個服務。上面的好處還在於,在你的 controller 中,你知道從 Service 返回來的所有東西都是 promise 對象;你不需要擔心它到底是 promise 還是 resouce 或者是 HttpPromise,這能讓你的代碼更加一致,並且可預測 - 因為 Javascript 是弱類型,並且到目前為止,據我所知沒有任何一款 IDE 能告訴你方法傳回值的類型,它只能告訴你開發人員寫了什麼注釋,這點上面就非常重要了。

實際鏈式例子

我們的程式碼程式庫有一部分是依賴於前一個調用的結果來執行的。Promise 非常適用這種情況,並且允許你書寫易於閱讀的代碼,儘可能保持你的代碼整潔。考慮如下例子:

angular.module(‘WebShopApp‘)  .controller(‘CheckoutCtrl‘, function($scope, $log, CustomerService, CartService, CheckoutService) {    function calculateTotals(cart) {      cart.total = cart.products.reduce(function(prev, current) {        return prev.price + current.price;      };      return cart;    }    CustomerService.getCustomer(currentCustomer)      .then(CartService.getCart) // getCart() needs a customer object, returns a cart      .then(calculateTotals)      .then(CheckoutService.createCheckout) // createCheckout() needs a cart object, returns a checkout object      .then(function(checkout) {        $scope.checkout = checkout;      })      .catch($log.error)    });

聯合非同步擷取資料(customers, carts,建立 checkout)和處理同步資料(calculateTotals);這個實現不知道,甚至不需要知道這些服務是不是非同步,它會等到方法之行結束,不論非同步與否。在這個例子中,getCart()會從本機存放區中擷取資料, createCheckout() 會執行一個 HTTP 要求來確定產品的採購,諸如此類。不過從使用者的視角來看(執行這個調用的人),它不會關心這些;這個調用起作用了,並且它的狀態非常明了,你只要記住前一個調用會將結果返回傳遞到下一個 then()

當然,它就是自注釋代碼,並且很簡潔。

測試 Promise - 基於代碼

測試 Promise 非常簡單。你可以硬測,建立你的測試類比對象,然後暴露 then() 方法,這種直接測法。但是,為了讓事情簡單,我只用了 $q 來建立 promise - 這是一個非常快的庫。下面嘗試示範如何類比上面用到過的各種服務。注意,這非常冗長,不過,我還沒有找出一個方法來解決它,除了在 promise 之外弄一些通用的方法(指標看起來更短更簡潔,會比較受歡迎)。

describe(‘The Checkout controller‘, function() {  beforeEach(module(‘WebShopApp‘));  it(‘should do something with promises‘, inject(function($controller, $q, $rootScope) {    // create mocks; in this case I use jasmine, which has been good enough for me so far as a mocking library.    var CustomerService = jasmine.createSpyObj(‘CustomerService‘, [‘getCustomer‘]);    var CartService = jasmine.createSpyObj(‘CartService‘, [‘getCart‘]);    var CheckoutService = jasmine.createSpyObj(‘CheckoutService‘, [‘createCheckout‘]);    var $scope = $rootScope.$new();    var $log = jasmine.createSpyObj(‘$log‘, [‘error‘]);    // Create deferreds for each of the (promise-based) services    var customerServiceDeferred = $q.defer();    var cartServiceDeferred = $q.defer();    var checkoutServiceDeferred = $q.defer();    // Have the mocks return their respective deferred‘s promises    CustomerService.getCustomer.andReturn(customerServiceDeferred.promise);    CartService.getCart.andReturn(cartServiceDeferred.promise);    CheckoutService.createCheckout.andReturn(checkoutServiceDeferred.promise);    // Create the controller; this will trigger the first call (getCustomer) to be executed,    // and it will hold until we start resolving promises.    $controller("CheckoutCtrl", {      $scope: $scope,      CustomerService: CustomerService,      CartService: CartService,      CheckoutService: CheckoutService    });    // Resolve the first customer.    var firstCustomer = {id: "customer 1"};    customerServiceDeferred.resolve(firstCustomer);    // ... However: this *will not* trigger the ‘then()‘ callback to be called yet;    // we need to tell Angular to go and run a cycle first:    $rootScope.$apply();    expect(CartService.getCart).toHaveBeenCalledWith(firstCustomer);    // setup the next promise resolution    var cart = {products: [ { price: 1 }, { price: 2 } ]}    cartServiceDeferred.resolve(cart);    // apply the next ‘then‘    $rootScope.$apply();    var expectedCart = angular.copy(cart);    cart.total = 3;    expect(CheckoutService.createCheckout).toHaveBeenCalledWith(expectedCart);    // Resolve the checkout service    var checkout = {total: 3}; // doesn‘t really matter    checkoutServiceDeferred.resolve(checkout);    // apply the next ‘then‘    $rootScope.$apply();    expect($scope.checkout).toEqual(checkout);    expect($log.error).not.toHaveBeenCalled();  }));});

你看到咯,測試 promise 的代碼比它自己本身要長十倍;我不知道是否/或者有更簡單的代碼能達到同樣目的,不過,也許這裡應該還有我沒找到(或者發布)的庫。

要擷取完整的測試覆蓋,需要為三個部分都編寫測試代碼,從失敗到處理結束,一個接一個,確保異常被記錄。雖然代碼中沒有很清楚示範,但是代碼/處理實際上會有許多分支;每個 promise 到最後都會被解決或者拒絕;真或假,或者被建立分支。不過,測試的粒度到底是由你決定的。

我希望這篇文章給大家帶來一些理解 promise 的啟示,以及教會怎樣結合 Angular 來使用 promise。我覺得我只摸到了 一些皮毛,包括在這篇文章以及在到目前為止我所做過的 AngularJS 工程上;promise 能夠擁有如此簡單的 API,如此簡單的概念,並且對大多數 Javascript 應用來說,有如此強大的力量和影響有點難以置信。結合高水平的通用方法,程式碼程式庫,promise 可以讓你寫出更乾淨,易於維護和易於擴充的代碼;添加一個控制代碼,改變它,改變實現方式,所有這些東西都很容易,如果你對 promise 的概念已經理解了的話。

從這點考慮,NodeJS 在開發早期就拋棄了 promise 而採用現在這種回調方式,我覺得非常古怪;當然我還沒有完全深入理解它,但是看起來好像是因為效能問題,不符合 Node 的原本目標的緣故。如果你把 NodeJS 當成一個底層的庫來看的話,我覺得還是很有道理的;有大量的庫可以為 Node 添加進階的 promise API(比如之前提到的 Q).

還有一點請記住,這篇文章是以 AngularJS 為基礎的,但是,promises類promise 編程方式已經在 Javascript 庫中存在好幾年了;jQuery ,Deferreds 早在 jQuery 1.5 (1月 2011) 就被添加進來。雖然看起來一樣,但不是所有外掛程式都能用。

同樣,Backbone.js 的 Model Api 也暴露了 promise 在它的方法中(save() 之類),但是,以我的理解,它貌似沒有沿著模型事件真正的起作用。也有可能我是錯的,因為已經有那麼一段時間了。

如果開發一個新的 webapp 的時候,我肯定會推薦基於 promise 的前端應用的,因為它讓代碼看起來非常整潔,特別是結合函數式編程範式。還有更多功能強勁的編程模式可以在 Reginald BraithwaiteJavascript Allongé book 中找到,你可以從 LeanPub 拿到免費的閱讀副本;還有另外一些比較有用的基於 promise 的代碼。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.