— Tech+Life+Music

Archive
Tag "promise"

Deferred objects are a new addition to jQuery 1.5, and are meant to strike a cleaner division between executing a task and waiting for the task to complete (and reacting accordingly). In this article, we’ll talk about what deferred objects are and what they can do for you, and finish everything off with a simple example application.

If you’ve worked with Javascript a fair deal, then it’s probable that you’ve run across callback functions before. These are most prevalent in asynchronous operations (AJAX comes to mind, of course), and basically provide a way to execute something when an asynchronous operation completes (e.g. react accordingly when an AJAX operation succeeds or fails).

Let’s take a vanilla pre-jQuery 1.5 AJAX request as an example:

$.ajax({
    url : 'ajaxservice.svc/method',
    data : '{}',
    success : function () { alert('YAY!'); },
    error : function () { alert('gulp!'); }
});

Now, with the advent of deferred objects in jQuery 1.5, all $.ajax() derivatives now return deferred objects, which allow you to do the following:

var $defer = $.ajax({
    url : 'ajaxservice.svc/method',
    data : '{}'
});
 
$defer.success(function () { alert('YAY!'); })
      .error(function () { alert('gulp!'); })
      ;

“So what?”, you may say. Well, if you haven’t noticed, the syntax on registering function callbacks is now more akin to the standard way of registering handlers in jQuery, such as with .click() or even .bind() and .delegate(). This allows us to chain registrations into the more familiar way of doing it in jQuery.

var $defer = $.ajax({
    url : 'ajaxservice.svc/method',
    data : '{}'
}).success(function () { alert('YAY!'); })
  .error(function () { alert('gulp!'); })
  ;
 
$defer.success( function () { /* perform additional operations */ } );

The less-noticed advantage of this syntax (and, in my opinion, it’s greatest boon) is how it enables us to easily slap on multiple handlers onto the task’s result events, just like with the vanilla jQuery events such as .click(). The example above shows just that: we registered two separate function literals as two separate handlers for the success callback of a single AJAX operation.

// remember why we stopped doing this in the first place?
<a onclick="alert('DOM Level 0 click handler!');" href="#">Argh!</a>

The old JSON parameter syntax of registering handlers on AJAX calls only allowed us to slap on one callback per event per call. Of course, we could go and create a function literal that calls all the necessary function handlers, but as systems scale and grow more complex, it gets harder and harder to maintain and keep track of everything that’s expected to happen when something happens.

Taking stuff one notch higher

While deferred objects make it so much easier to attach multiple event handlers on a single asynchronous call, that’s easily eclipsed by the fact that deferred objects also allow you to do the exact opposite: performing a task once several tasks are completed.

Let’s tackle this with a more real-world example. Imagine that we had a web page that makes two AJAX calls: one to get data from the server, and another to get an HTML template markup snippet. What we aim to do is that when the two AJAX calls resolve, we want to get our HTML template, slap the fetched content somewhere in it, and append the result into the current page’s DOM.

var flags = [];
 
var my_handler = function () {
    if (flags.length == 2) {
        // do something with the data and HTML template in flags
    }
};
 
$.ajax({
	url : 'ajax.svc/getdata',
	success : function (m) {
	    flags['data'] = m.d;
	    my_handler();
	}
});
 
$.ajax({
	url : 'ajax.svc/gettemplate',
	success : function (m) {
	    flags['template'] = m.d;
	    my_handler();
	}
});

The code above definitely has a number of improvement points going for it, but it effectively illustrates how we may go about with doing the business case we raised. While the code is easily readable and effectively clear, it suffers from the same problems that scalable applications aim to eliminate: as the code expands and the system grows more complex, we’re looking at maintaining larger and larger blocks similar to that above, and eventually that will just be too difficult to maintain and someone will likely trip up.

Along with deferred objects, jQuery also exposes the new $.when() utility function that takes care of managing tasks similar to the one above.

$.when(
    $.ajax({ url : 'ajax.svc/getdata' }),
    $.ajax({ url : 'ajax.svc/gettemplate' })
).then(function (data, template) {
    // do something with the data and template
});

$.when() accepts any number of deferred objects (which $.ajax() derivatives now return), and automatically attach event handlers to the tasks as a singular collective. The resulting data from each deferred object are mapped to the handler function’s parameters in the same order as they were declared (i.e. the resulting data from $.ajax({ url : 'ajax.svc/getdata' }) is mapped to data in the function literal in the .then() call, and so on).

This construct accepts three primary handlers:

  • .done() – called when all deferred tasks succeed
  • .fail() – called when at least one of the deferred tasks fail
  • .complete() – called as soon as all deferred tasks complete, whether or not they succeeded or not

The .then() is shorthand for declaring both .done() and .fail() : the first parameter takes the on-success callback, while the second takes the on-fail callback.

Creating a deferred object

While $.ajax() derivatives now automatically return a deferred object, you may want to manually create deferred objects in your code for specific purposes.

To create a deferred object, you simply call $.Deferred(). These objects basically have three states: unresolved, resolved and rejected.

Deferred objects start off as unresolved, and are either resolved or rejected based on a task’s outcome. When manually handling deferred objects, you’d want to either call .resolve() or .reject() based on a task’s outcome.

Let’s build our own deferred logic example

To put everything we’ve discussed together, we’ll try to create a very simple system that makes use of deferreds.

We’ll use an application that has three INPUT fields. The user is expected to fill up all three fields, so we want a task to run when all three fields have been completed. For this example, we’re going to use deferred objects to update a simple message when all three fields have been filled up by the user.

To start off, here is our completed application:

Form is incomplete.
First Name
Second Name
Third Name

Let’s get started!

We’ve got three INPUT fields that we want filled, so it makes sense to create three separate deferred objects; one for each field.

var $deferreds = [];
// our INPUTs are inside a DIV#example-pane, so we set the context to that
$('input','#example-pane').each(function(i,e){
    $deferreds[i] = $.Deferred();
});

We then initialize the task to perform once all our deferred objects resolve. In this case, we just want to change the text in a simple SPAN element.

// we use the JS .apply() method to call the $.when() function with the elements
// of an array laid out as the function arguments
$.when.apply(null, $deferreds).then(function(){
    $('#example-pane span').text('complete').css('color','#6d6');
});

Finally, we need to manually resolve our deferred objects at the proper time. In our example, we want to resolve the corresponding deferred object when the value of an INPUT field is changed.

$('input','#example-pane').change(function(){
    var myIndex = $('#example-pane').find('input').index(this);
    $deferreds[myIndex].resolve();
});

… aaaaaaaaaaand we’re done. Our complete code is below:

$(function($){
 
    var $deferreds = [];
 
    $('input','#example-pane')
        .each(function(i,e) {
            $deferreds[i] = $.Deferred();
        })
        .change(function(){
            $deferreds[$('#example-pane').find('input').index(this)].resolve();
        });
 
    $.when.apply(null,$deferreds).then(function(){
        $('span','#example-pane').text('complete').css('color','#6d6');	
    });
 
});

Our example can use some improvements (like caching $('#example-pane') for example), but hopefully it has been a clear illustration of how jQuery deferred objects can work with you in your code.

Read More