Pages

Tuesday, September 4, 2012

jQuery Function Of The Week: Deferred (deep dive)

This week we'll take a deeper look into jQuery's Deferred object. According to the jQuery docs:

jQuery.Deferred(), introduced in version 1.5, is a chainable utility object that can register multiple callbacks into callback queues, invoke callback queues, and relay the success or failure state of any synchronous or asynchronous function.

That is a bit of a mouthful. I like to think of deferrds somewhat like a telephone call. You'd like to contact someone so you dial a number. Based on what happens when you call that number you will take action. If the callee answers the call you can start a conversation, if you get their voice mail (I almost said answering machine but I'm afraid that some readers will have no idea what that is) you can take leave a message. Also, when the phone is ringing you can even do something else, you only care about what happens once the number you are calling responds in some way. This is an extremely simplified way of describing how Deferreds work because Deferrds can do so much more.

If you've used jQuery's AJAX functions you've already been taking advantage of Deferreds, the AJAX functions are actually using Deferrds behind the scenes with a lot of the details abstracted out for you.

When talking about Deferreds its important to note that there are two parts to each Deferred implementation: the callers and the callees. The callers are the functions that either do something that other functions are waiting for or functions that take action based on the results of another function. The callees are the functions that depend on the results of at least one of the callee functions.

The Callers

Like I mentoned before, the caller functions are divided into two parts: the functions that do something that other functions depend on, and the functions that take action based on the the results of other functions. The when(deferrds)function is the first part of the callers and is a wrapper for all of the functions have results that other functions are waiting for. jQuery will execute the functions passed into when immediately and asynchrouously. This means if we have the following code:

func1(), func2(), and func()will all be called at approximately the same time. Functions passed into when are expected to return a promise object back to the when() function. This promise will either be resolved or rejected (more on these in the callees section) by the function passed into when, and based on this result one of callback functions will be called. The second part of the callers are the functions that respond to the results of the functions passed into the when function. The three functions are: then, done, and fail. The then(doneCallbacks, failCallbacks) is a single method that encapsulates the functionality of the done() and fail() callbacks. The first argument to the then function is a function, or array of functions, that will be called when all the functions passed into the when() function are resolved. The second argument passed into the then function is a function, or array of functions, that will be immediately invoked as soon as any of the functions passed into the when() function are rejected. The then and done functions will receive arguments from each function passed into the when function, in the order they were passed. These arguments are passed into them via the resolve and reject methods (we will talk about when we discuss the callees). If you'd like to be a bit more expressive in your code instead of using the then() you can specify the doneCallbacks and failCallbacks in the done() and fail() methods. These methods are called the same was as the doneCallbacks, and failCallbacks of the then() function.The CalleesWhen functions are passed into the when function jQuery expects these functions to return a promise to it. A promise is just a message telling the when function that sometime in the future the function is either going to send it a reject( args ) or resolve( args ) message. Once the when function receives the promise object from a function it just sits and waits for a response. The callee functions can also pass information back to the when function by passing arguments into the reject and resolve methods (I'll show you an example of this in the real world example). Lets take a look at a typical callee function that implements the Deferred object and talk a bit more about what is going on:

Here we have a function that is going to implement the $.Deferred() function and immediately return a promise message to the when() function that called it. The $.Deferred() constructor function takes an optional function as an argument which is passed a reference to itself as an argument. The above function could also have been rewritten as:

Its important to note that if a function passed into when does not implement this Deferred pattern it will immediately be treated as resolved.

Real World Example

Now that we've looked at the parts of the Deferred lets take a look at a real world example. In order to demonstrate the power of Deferred's lets take a look at some code that can benefit from them:JS Bin

The intent of this code is to fade out any current content, get some tweets from twitters search API, and finally display the new tweets. When any of the buttons are pressed you'll quickly see that something isn't right, and this is because the code being called relies on asynchronous operations like doing an AJAX call and jQuery animations.

One approach to fix this would be to put the getTweets function as a callback to the cleanDiv function's fadeOut callback, move the addTweets function to the ajax calls success callback, and finally move the the $tweetDiv.fadeIn('slow') code to the addTweets function. Doing so would tightly couple this code together making it tough to extend and maintain. A better solution would to use the power of jQuery's Deferred object.

First lets look at our click handler function rewritten to take advantage of the Deferred object:

The first thing you probably notice is that there is more code in the new click handler. The Deferred calls do add a bit of code, but I feel this code is quite expressive (meaning that the intent of the code is very clear) and each of the callback functions are decoupled from any function they are waiting on. One other thing to note here is the use of the done() and fail() callbacks. In the done() callback I use the when() and then() methods of the Deferred implementation. Here I am only specifying a single argument which will get called if/when the addTweets function passed into it has completed. Since I'm not going to reject the promise anywhere in the addTweets function only passing in a done callback is sufficient.

Now lets take a look at one of the new callee functions. Here is the code for the new getTweets function:

Since there is no access to the internal representation of the $.ajax method's Deferred function I have to wrap the entire function in a Deferred call. Another thing to note is that Twitter's API will return a 200 status (which will invoke the ajax function's success callback) even if there is an error so I will inspect the object returned from Twitter when determining weather to resolve or reject the Deferred object. If a key of "error" is found in the data returned from Twitter I reject the Deferred which tells our click handler function to run the fail callback. I'll also pass a message to the fail callback through the reject method call.

Putting it all together we get a nice and smooth implementation of our application. As you can see I've also included an error button which will trigger an error in the twitter API.