Promise and Design Patterns in AngularJS

Source: Internet
Author: User

Promises and Design Patterns

Well written long good long long ~

The traditional way to solve a Javascript asynchronous event is to call a callback function, invoke a method, then give it a function reference, and execute the function reference when the method ends.

$.get (' Api/gizmo/42 ', function (Gizmo) {  console.log (gizmo);//or whatever});

It looks great, right, but there are drawbacks; first, merging or linking multiple asynchronous processes is super-complex; either a lot of template code, or, well, you know, callback Hell (a layer of callbacks):

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

You got it. In fact, in JavaScript, there is another asynchronous processing mode: More dick, often called in JavaScript Promises , the CommonJS standard board then released a specification, called the API Promises .

The concept behind Promise is very simple and has two parts:

    • Deferreds, define the unit of work ,
    • Promises, the data returned from the deferreds.

Basically, you'll use Deferred as a communication object to define the start, process, and end of the work cell.

Promise is the output of the Deferred response data; it has a state (wait, execute, and reject), and a handle, or callback function, which is the method that will be called in the process of Promise execution, rejection, or prompting.

Promise a very important point that differs from the callback is that you can append the handle to the Promise state after it becomes Execution (resolved) . This allows you to transfer data, ignoring whether it has been acquired by the app, then caching it, and so on, so you can perform operations on the data regardless of whether it is already or is about to be available.

In the following article, we will explain the Promises based on AngularJS. The entire code base of AngularJS relies heavily on Promise, including the framework and the application code you write with it. AngularJS with its own Promises implementation, $q service, and a Q library of light-weight implementation.

$qImplements all of the deferred/promise methods mentioned above, in addition to $q its own implementation: to $q.defer() Create a new Deferred object $q.all() , allow to wait for more Promises execution end, there are methods $q.when() and , we'll talk about that later.

$q.defer()Returns a Deferred object, with methods resolve() , reject() and notify() . Deferred also has a promise property, which is an promise object that can be used to apply internal delivery.

The Promise object has three additional methods: .then() , is the only method promise the specification requirements, with three callback methods as parameters; A successful callback, a failure callback, and a state change callback.

$qTwo methods were added on top of the Promise specification: catch() It can be used to define a common method that is called when there is a Promise processing failure in the Promise chain. Also finally() , whether promise execution is a success or failure will be performed. Note that these should not be confused with Javascript exception handling or used: Exceptions thrown inside promise are not catch() captured. (※ It seems I understand wrong here)

Promise Simple Example

Here is a $q Deferred simple example of using,, and Promise putting together. First I would like to state that the code for all the examples in this article has not been tested, and that there is no correct reference Angular to services and dependencies, and so on. But I think it's good enough to enlighten you on how to play.

First, we start by creating a new unit of work, through the Deferred object, using $q.defer() :

var deferred = $q. Defer ();

Then we get it from Deferred promise and add some behavior to it.

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

Finally, we pretended to do something and then told deferred we had done it:

Deferred.resolve (' All done! ');

Of course, this does not need to be really asynchronous, so we can use the Angular $timeout service (or Javascript setTimeout , however, in the Angular application it is best to use $timeout , so you can mock/test it) to pretend.

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

Well, the interesting thing is: we can append a lot then() to a promise, and we can append after promise is 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);

Well, what if something happens? We use the deferred.reject() second function that it will start then() , just like a callback.

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()the second parameter, there is another option, you can use the chain catch() , in the Promise chain when an exception occurs when it will be called (probably after a lot of chain).

Promise  . Then (function success (data) {    console.log (data);  })  . catch (function error (msg) {    console.error (msg);  });

As an addition, for long time-consuming processing (such as uploads, long computations, batches, and so on), you can use the deferred.notify() then() third parameter to give promise a listener to update the status.

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 >=) {    $interval. Cancel (interval);    Deferred.resolve (' All done! ');  }  Progress + = ten;  Deferred.notify (progress + '% ... '); }, 100)
Chain-Type Promise

Before we have seen, you can give a promise append multiple processing ( then() ). The fun part of the Promise API is to allow chained processing:

Promise  . Then (dosomething), then  (dosomethingelse)  . Then (Dosomethingmore)  . catch (LogError);

As a simple example, this allows you to divide your function calls into simple, single-purpose methods rather than a blanket of hemp; another benefit is that you can reuse these methods in a multi-promise task, just like you do a chained approach (such as a task list).

If you start the next asynchronous process with the result of the previous asynchronous execution, it will be more bull X. The default, a chain, like the one shown above, is to pass the previous execution result object to the next one then() . Like what:

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 ');

This will output the following results on the console:

Abc

Although the example is simple, you have not realized if then() return to another promise that strong. In this case, the next then() will be executed at the end of the promise. This mode can be used to put the HTTP request string above, for example (when a request depends on the results of the previous request):

var deferred = $q. Defer () var promise = deferred.promise;//Resolve it after a second$timeout (function () {  deferred.re Solve (' foo ');},;p romise. Then  (function (one) {    console.log (' Promise one resolved with ', one);    var anotherdeferred = $q. Defer ();    Resolve after another second    $timeout (function () {      anotherdeferred.resolve (' Bar ');    }, +);    return anotherdeferred.promise;  })  . Then (function () {    console.log (' Promise-resolved with ', and ');  });

Summarize:

    • The Promise chain will pass the previous then return result to the next then (if not undefined) of the call chain.

    • If you then return a Promise object, the next one then will only be called when the promise is processed.

    • catchprovides an exception-handling point for the entire chain at the end of the chain

    • The end of the chain finally will always be executed, regardless of whether the promise is processed or rejected, and the cleanup function

      Parallel Promises and ' promise-ifying ' Plain Values

I also mentioned $q.all() that allows you to wait for parallel promise processing, when all promise are processed to the end, calling a common callback. In Angular, there are two ways to call this method: in a way or in a way Array Object . The Array method receives multiple promise, and then .then() uses a data result object at the time of the call, including all the promise results in the resulting object, in the order of the input arrays:

$q. All ([Promiseone, Promisetwo, Promisethree])  . Then (function (results) {    console.log (results[0], results [1], results[2]);  });

The second way is to receive a promise collection object that allows you to give each promise an alias that can be used in the callback function (with better readability):

$q. All ({first:promiseone, second:promisetwo, third:promisethree})  . Then (function (results) {    Console.log ( Results.first, Results.second, Results.third);  });

I recommend using array notation if you just want to be able to batch the results, that is, if you treat all the results equally. The object-based approach is more appropriate when you need self-explanatory code.

Another useful way to do this is $q.when() if you want to create a promise from a common variable, or if you're not sure if the object you're dealing with is not promise useful.

$q. When (' foo ') and then  (function (bar) {    console.log (bar);  }); $q. When (apromise) and then  (function (baz) {    console.log (baz);  }); $q. When (valueorpromise)-Then  (function (BOZ) {//-well-get the idea    .  })

$q.when()Caching in a service like this is also useful:

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 this instead of a separate Promise object    return Myresource.get (). $promise      . Then (function (someth ing) {        cachedsomething = something};};}  );

You can then call it this way:

Myservice.getsomething ()    . Then (function (something) {        console.log (something);    });
The practical application in AngularJS

In Angular I/O, most of the promise or Promise-compatible ( then-able ) objects are returned, but they are very strange. The $http document says that it will return an HttpPromise object, well, indeed promise, but there are two additional (useful) methods that should not scare jQuery users. It defines the success() error() first and second parameters that are used to correspond to each then() other.

The Angular $resource service, used for rest-endpoints $http encapsulation, is also a bit odd; the generic method ( get() , save() four of the like) receives the second and third arguments as success and error callbacks, and they also return an object, When the request is processed, the requested data is populated. It does not directly return the Promise object; instead, get() the object returned by the method has an attribute to $promise expose the Promise object.

On the one hand, this $http is inconsistent, and everything Angular is/should be promise, but on the other hand, it allows developers to simply $resource.get() assign the results $scope . Originally, the developer can assign $scope any promise, but starting from Angular 1.2 is defined as obsolete: see this commit where it is deprecated.

Personally, I prefer the unified API, so I encapsulate all I/O operations Service and return an object uniformly, promise but the call is $resource a bit rough. Here's an example:

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

This example is a bit obscure, because passing the ID parameter to BarResource look a bit redundant, but it also makes sense, such as you have a complex object, but only need to use its ID property to invoke a service. The upside is that, in your controller, you know that everything that Service comes back is an promise object; you don't have to worry about whether it's promise or resouce or HttpPromise , which makes your code more consistent and predictable-because Javascript is a weak type, and so far as I know that no IDE can tell you the type of the return value of the method, it can only tell you what comments the developer wrote, which is very important.

Actual Chain Example

Part of our code base is executed by relying on the results of the previous call. Promise is very well suited to this situation and allows you to write easy-to-read code to keep your code tidy as much as possible. Consider the following example:

angular.module (' Webshopapp '). Controller (' Checkoutctrl ', function ($ Scope, $log, CustomerService, Cartservice, Checkoutservice) {function calculatetotals (cart) {cart.total = CART.P      Roducts.reduce (function (prev, current) {return prev.price + Current.price;      };    return cart; } customerservice.getcustomer (Currentcustomer). Then (Cartservice.getcart)//Getcart () needs a customer object, re Turns a cart calculatetotals then (checkoutservice.createcheckout)//Createcheckout () needs a cart objec      T, returns a checkout object. Then (function (checkout) {$scope. Checkout = checkout; }). catch ($log. Error)}); 

The Union asynchronously obtains the data ( customers , carts creates checkout ) and processes the synchronized data ( calculateTotals ); This implementation does not know, and does not even need to know whether these services are asynchronous, it will wait until the end of the line of the method, whether asynchronous or not. In this example, the getCart() data is fetched from the local store, createCheckout() an HTTP request is executed to determine the purchase of the product, and so on. However, from the user's point of view (the person executing the call), it does not care about these; The call works, and its state is very clear, just remember that the previous call will pass the result back to the next one then() .

Of course, it's self-explanatory code, and it's simple.

Test Promise-code-based

Testing Promise is very simple. You can hardcode, create your test mock object, and then expose the then() method to this direct measurement method. But, in order to make things simple, I only used $q to create promise -this is a very fast library. Here's an attempt to demonstrate how to simulate the various services used above. Note that this is very lengthy, but I haven't figured out a way to fix it, except to get some general-purpose methods outside of promise (the pointer looks shorter and more concise, which is more popular).

Describe (' The Checkout controller ', function () {Beforeeach (module (' Webshopapp ')); It (' should do something with promises ', inject (function ($controller, $q, $rootScope) {//create mocks;    Use Jasmine, which have 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 (); The mocks return their respective deferred ' s promises CustomerService.getCustomer.andReturn (Customerservicedefe    Rred.promise); CartService.getCart.andReturn (cartservicedeferred. Promise);    CheckoutService.createCheckout.andReturn (checkoutservicedeferred.promise); Create the Controller;    This would trigger the first call (GetCustomer) to be executed,//and it'll 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-be called yet;    We need to the 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.resol    ve (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 (); }));});

You see, promise the code for the test is 10 times times longer than it itself, and I don't know if/or there's a simpler code that can do the same thing, but maybe there's a library I didn't find (or publish).

To get a complete test overlay, you need to write the test code for three parts, from the failure to the end of the process, one by one, to ensure that the exception is logged. Although there is no clear demonstration in the code, there are actually many branches of code/processing, and each promise will be resolved or rejected at the end, true or false, or set up as a branch. However, it is up to you to determine the granularity of the test.

I hope this article will bring you some insight into the promise and how the church can combine Angular to use promise. I think I only touched some fur, including in this article and in the AngularJS project I've done so far; promise can have such a simple API, such a simple concept, and for most Javascript applications, There is such a strong power and influence a little unbelievable. Combined with a high level of common approach, code base, Promise allows you to write cleaner, easier-to-maintain and easy-to-extend code, add a handle, change it, change how it's done, and all of these things are easy if you understand the concept of promise.

From this point of view, NodeJS abandoned promise in the early stages of development and adopted the current callback method, which I think is very odd, and of course I haven't fully understood it yet, but it seems to be because of performance problems that do not conform to Node's original goal. If you look at NodeJS as a bottom-level library, I think it makes sense to have a lot of libraries that can add advanced promise APIs to Node (like the Q mentioned earlier).

And, remember, this article is based on AngularJS, but, promises and 类promise programmatically, has been in Javascript libraries for years, and jQuery Deferreds was added in JQuery 1.5 (January 2011). 。 Although it looks the same, not all plugins are available.

Similarly, Backbone.js the model API is exposed to promise its methods (and the save() like), but, in my understanding, it does not seem to really work along model events. It's also possible that I'm wrong, because it's been a while.

If I were to develop a new webapp, I would definitely recommend a promise-based front end application because it makes the code look neat, especially in conjunction with functional programming paradigms. There are more powerful programming patterns that can be found in Reginald Braithwaite Javascript Allongé book , where you can get a free copy of your reading from leanpub, and some more useful promise-based code.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

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.