Using jQuery's Deferred Functionality For Asynchronous Configuration

Yesterday, I started looking into a JavaScript pub/sub (publish and subscribe) library that provided asynchronous utility methods. These methods provided UUIDs (universally unique identifiers) and normalized timestamps in order to simplify the development of realtime applications across a global client base. These utility methods made connections to the server and then used a callback-based approach to return the requested value. When I saw that I needed to use multiple asynchronous utility methods in order to fully configure a given entity, I thought it would be the perfect opportunity to use jQuery's Deferred functionality.

It's been a long time since I've looked at Deferred objects in jQuery, so I'll give you the briefest of rundowns. Deferred objects are event "emitting" objects whose encapsulated value will be available at some point in the future (including "now"). When those encapsulated values become available, the deferred object will be considered "resolved" and any callbacks subscribed to the "done" event will be invoked. Conversely, if the encapsulated value fails to load, any callbacks subscribed to the "fail" event will be invoked.

In recent releases of jQuery, the entire AJAX module was rewritten to work with Deferred objects. Additionally, a Deferred constructor method has been provided such that we can wrap any piece of logic with deferred functionality. So, when I saw that this pub/sub API provided multiple, asynchronous utility methods, it seemed like a workflow perfectly suited for deferred management.

In the following demo, I have a user object that I have to configure with a UUID and a timestamp. Both of these methods use a callback-based response mechanism:

getUUID( callback )

getTime( callback )

In order to make sure that both methods return before I attempt to make use of the user object, I have wrapped them in Deferred functionality and then further packaged them together with a jQuery.when() based deferred workflow.

<!DOCTYPE html>

<html>

<head>

<title>Using jQuery Deferred For Asynchronous Configuration</title>

<script type="text/javascript" src="../jquery-1.6.1.js"></script>

<script type="text/javascript">

// *****************************************************

// *****************************************************

//

// NOTE: If I had control over the API requests, I might

// just return a PROMISE from each method, rather than take

// a callback. I am using the callback approach to mirror

// the 3rd-party API that I was actually dealing with:

//

// -- getUUID( callback )

// -- getTime( callback )

//

// *****************************************************

// *****************************************************

// I get the UUID from the server. Since this request is

// asynchronous, I will pass the result off to the callback.

function getUUID( callback ){

// Make the request to the server.

var request = $.ajax({

type: "get",

url: "./get_uuid.cfm",

dateType: "text"

});

// Handle the success / fail responses.

request.then(

function( uuid ){

// Invoke the callback with the given UUID.

callback( uuid );

},

function(){

// Invoke the callback with null.

callback( null );

}

);

}

// I get the normalized time from the server. Since this

// request is asynchronous, I will pass the result off to

// the callback.

function getTime( callback ){

// Make the request to the server.

var request = $.ajax({

type: "get",

url: "./get_time.cfm",

dateType: "text"

});

// Handle the success / fail responses.

request.then(

function( milliseconds ){

// Invoke the callback with the given date/time.

// The milliseconds are coming back as a string;

// we are multiplying by 1 to convert to int.

callback( new Date( milliseconds * 1 ) );

},

function(){

// Invoke the callback with null.

callback( null );

}

);

}

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// This is our default user configuration. We will need to

// load the UUID and the timestamp from the server in order

// to run the rest of the code.

var user = {

uuid: null,

dateCreated: null

};

// Load all the configuration data. Once it is all loaded, we

// will be able to run the rest of the scripts.

var init = $.when(

// Get the UUID.

$.Deferred(

function( deferred ){

// Get the uuid; when it comes back, store it in

// the user congiruation and resolve the deferred.

getUUID(

function( uuid ){

// Store the UUID in the user.

user.uuid = uuid;

// Reslove the when-based deferred object.

deferred.resolve();

}

);

}

),

// Get the time.

$.Deferred(

function( deferred ){

// Get the time; when it comes back, store it in

// the user congiruation and resolve the deferred.

getTime(

function( dateCreated ){

// Store the date/time object in the user.

user.dateCreated = dateCreated;

// Reslove the when-based deferred object.

deferred.resolve();

}

);

}

),

// DOM-ready event.

$.Deferred(

function( deferred ){

$( deferred.resolve );

}

)

);

// -------------------------------------------------- //

// -------------------------------------------------- //

// When the user has finished being initialized, configure

// the rest of the scripts (ie. where we would start doing

// stuff with the fully loaded user).

init.done(

function(){

// Log the inidialized user settings.

console.log( "Initialized User" );

console.log( "UUID:", user.uuid );

console.log( "Created:", user.dateCreated );

}

);

</script>

</head>

<body>

<!-- Left intentionally blank. -->

</body>

</html>

As you can see, I am using the jQuery.when() method in order to group together three asynchronous events: getting the uuid, getting the time, and waiting for the DOM to load. In order to use each of these three asynchronous events within the jQuery.when() method, I have wrapped them in Deferred objects. In this way, the callback used in getUUID() and getTime() can be made to resolve the deferred container.

Once all the deferred objects have been resolved (or failed), the jQuery.when() deferred object invokes its own callbacks; and, when we run the above code, we get the following console output:

As you can see, at the time of the logging, both asynchronous utility methods have returned successfully.

While it is not entirely relevant to the Deferred exploration, I'll show you the ColdFusion code that powered the two AJAX requests:

get_uuid.cfm

<!--- Sleep the thread slightly for demo purposes. --->

<cfset sleep( 2 * 1000 ) />

<!--- Create a UUID value. --->

<cfset uuid = createUUID() />

<!--- Set the content type and stream it back to the client. --->

<cfcontent

type="text/plain"

variable="#toBinary( toBase64( uuid ) )#"

/>

As you can see, I am sleeping this thread for 2 seconds in order to test a delayed response in the deferred workflow.

get_time.cfm

<!--- Create the normalized time. --->

<cfset normalizedTime = getTickCount() />

<!--- Set the content type and stream it back to the client. --->

<cfcontent

type="text/plain"

variable="#toBinary( toBase64( normalizedTime ) )#"

/>

When I first heard about Deferred objects in jQuery, I wasn't really sure why they'd be useful. Now that I've had a few chances to play with them, however, I absolutely love them. While jQuery's AJAX functionality was always easy to use with a single request, there was never a great way to manage parallel requests. By creating deferred objects, we can now spawn, manage, and respond to all manor of asynchronous functionality with ease.

Exactly - the Reset attribute actually defaults to yes/true (so it resets the output buffer regardless). The biggest problem is that the Variable attribute can only take a binary value :( If you try to pass it a string, it will throw up the following exception:

Attribute validation error for tag cfcontent. java.lang.String is not a supported variable type. The variable is expected to contain binary data.

I think ColdFusion only ever expected people to use it with file content or PDF documents... but I like to use it with strings! Hence, the toBinary() intermediary step.

Honestly, I wish they would throw a type check inside and do the conversion implicitly. Perhaps a good Feature Request?

I go back and forth on the use of the underlying Java methods. I really do like the getBytes() approach; and I do use it sometimes. But then sometimes, I feel guilty about it not being documented.... my brain won't come to rest on the situation :)

Well you can add me to the list of people still having trouble grasping the concept of Deferreds, so I'm helpful for any other angles provided.

I do have one question regarding your example, though. Is there a specific reason (e.g. flexibility, performance, etc.) why you decided to go with request.then instead of inline success and error handlers within the AJAX call, or was it mostly for additional Deferred demonstration?

Great question. Now that deferreds allow me to break up the workflow a little bit, I'm finding that it has a couple of benefits:

1 - It cuts down on the indenting of the code (the traditional "boomerang" code that is so common in callback-driven style).

2 - I find that it helps me think a bit more clearly because I can segment my intents. Here's the code that makes the request; and here's the code that handles the response.

3 - It makes it easier for me to add comments. As you can probably see, I LOVE comments :) And, when I can break up lines, it allows for more white-space and commenting, which is something I seem to be addicted to.

All of these, however, are a personal preference. And, when I'm coding apps that have earlier versions of jQuery, I fallback to the inline callback notation without any problem. This approach just feels a few percent more enjoyable.

I am the co-founder and lead engineer at InVision App, Inc — the world's leading prototyping,
collaboration & workflow platform. I also rock out in JavaScript and ColdFusion 24x7 and I dream about
promise resolving asynchronously.