Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with:

Using Deferred Objects As An Asynchronous Script Loader In jQuery 1.5

By Ben Nadel on

After playing with Deferred Objects for the first time yesterday, I've definitely gotten deferred on the brain. I've known about deferred objects for a long time and never really understood them; as such, I'm quite eager to wrap my head around exactly what it is that they are capable of doing. Last night, just after I posted my previous blog entry, it occurred to me that they might be useful for asynchronous script loading.

 
 
 
 
 
 
 
 
 
 

One deferred-oriented function that I didn't talk about yesterday was the new $.when() method. The $.when() method accepts any number of Deferred objects and resolves once all of its deferred parameters get resolved. If any of its deferred parameters fails, the $.when() promise fails.

  • $.when( [Deferred [, Deferred ...]] ) :: Deferred (promise)

Since then $.when() function returns a Deferred promise, it means that you can then bind to the done(), fail(), and then() methods:

  • $.when().done( function | [function] )
  • $.when().fail( function | [function] )
  • $.when().then( done, fail )

As I demonstrated yesterday, jQuery 1.5's rewrite of the $.ajax() method now returns a Deferred promise object; that will help us when we use the short-hand $.getScript() function to load our remote script files. But what about the DOM-ready event? It's one thing to load scripts asynchronously - it's another to know that you can use the content that they make available.

In Eric Hynds' blog comments, Julian Aubourg demonstrated that the $.Deferred() constructor could accept a function. This function is given one parameter - a deferred object instance - which it could then use to resolve or reject the deferred object returned by the constructor.

  • $.Deferred( function( deferred ){ ... } ) :: Deferred

NOTE: The object returned from the $.Deferred() constructor is a Deferred object, not a promise; this means that it can still be used to mutate the state of the deferred instance.

Using this approach, we can easily create a Deferred object that resolves once jQuery's DOM-ready event has been triggered:

  • // Create a deferred object that hooks into the DOM-ready event.
  • var myDeferredObject = $.Deferred(
  • function( deferred ){
  •  
  • // Pass the resolve function as the DOM-ready event handler.
  • // Once the DOM is ready to be interacted with, jQuery will
  • // invoke the resolve method which will resolve the Deferred
  • // object returned from the Deferred() constructor.
  • $( deferred.resolve );
  •  
  • }
  • );

As you can see, we are using the resolve() method as the callback for the DOM-ready event. Now that we can do that for DOM-ready, we can easily use this deferred object in conjunction with a call to the $.when() method.

To play with this concept, I created a ColdFusion page that would dynamically generate our Javascript file. The ColdFusion code sleeps the incoming request in an attempt to mimic some network latency.

Script.cfm

  • <!--- Param the script ID (for identification in testing). --->
  • <cfparam name="url.id" type="numeric" />
  •  
  • <!--- Pause the script to simulate remote network load time. --->
  • <cfthread
  • action="sleep"
  • duration="#(randRange( 2, 5 ) * 1000)#"
  • />
  •  
  • <!--- Denote the content as Javascript. --->
  • <cfcontent type="text/javascript">
  •  
  • <cfoutput>
  •  
  • // Add some Javascript to help test if the script has been
  • // loaded by the deferred script loader.
  • var script#url.id# = {
  • id: #url.id#,
  • loaded: true
  • };
  •  
  • </cfoutput>

As you can see, each call to this remote script page will add a "script{N}" object to the global name space.

With this page in place, I then created a test page that would use jQuery 1.5's $.when() method to load many versions of the remote script:

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Using Deferred As A Script Loader In jQuery 1.5</title>
  • <script type="text/javascript" src="../jquery-1.5.js"></script>
  • <script type="text/javascript">
  •  
  • // Load a bunch of scripts and make sure the DOM is ready.
  • $.when(
  • $.getScript( "./script.cfm?id=1" ),
  • $.getScript( "./script.cfm?id=2" ),
  • $.getScript( "./script.cfm?id=3" ),
  • $.getScript( "./script.cfm?id=4" ),
  • $.getScript( "./script.cfm?id=5" ),
  • $.getScript( "./script.cfm?id=6" ),
  • $.getScript( "./script.cfm?id=7" ),
  • $.getScript( "./script.cfm?id=8" ),
  • $.getScript( "./script.cfm?id=9" ),
  • $.getScript( "./script.cfm?id=10" ),
  •  
  • // DOM ready deferred.
  • //
  • // NOTE: This returns a Deferred object, NOT a promise.
  • $.Deferred(
  • function( deferred ){
  • // In addition to the script loading, we also
  • // want to make sure that the DOM is ready to
  • // be interacted with. As such, resolve a
  • // deferred object using the $() function to
  • // denote that the DOM is ready.
  • $( deferred.resolve );
  • }
  • )
  • ).done(
  • function( /* Deferred Results */ ){
  • // The DOM is ready to be interacted with AND all
  • // of the scripts have loaded. Let's test to see
  • // that the scripts have loaded.
  • for (var i = 1 ; i <= 10 ; i++){
  •  
  • // Test to see if the contents of the downloaded
  • // script have been applied to the global name
  • // space (window).
  • console.log(
  • ("Script " + i + ":"),
  • window[ "script" + i ].loaded
  • );
  •  
  • }
  • }
  • );
  •  
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <h1>
  • Using Deferred As A Script Loader In jQuery 1.5
  • </h1>
  •  
  • </body>
  • </html>

As you can see, we are passing 10 deferred $.getScript() results and a one-off hook into the DOM-ready event to the $.when() method. Then, we take the promise returned by $.when() and define a done() callback. The done() callback will only be invoked once all of the scripts and the DOM-ready deferred objects have been resolved. At that point, we loop over the scripts to make sure that they have loaded. Doing so results in the following console output:

Script 1: true
Script 2: true
Script 3: true
Script 4: true
Script 5: true
Script 6: true
Script 7: true
Script 8: true
Script 9: true
Script 10: true

As you can see, each script was successfully loaded before the done() callback was invoked.

When using the $.when() method in this way, there is no contract as to the order in which the Deferred arguments will be resolved. As such, there is no guarantee that the scripts will load in a top-down manner; the only promise is that they will all load before the done() callbacks get invoked. If the order of the scripts is important, I suppose you'd need something like LAB.js.




Reader Comments

Hey Ben,

I noticed it's possible to return a promise with the

  • $.Deferred(fn)

signature simply by chaining a

  • .promise()

call after creating a deferred. I think this was just an oversight by Julian when he posted that example on my blog.

Applying this to your example, you could instead write:

  • $.when(
  • $.getScript( "./script.cfm?id=1" ),
  • $.getScript( "./script.cfm?id=2" ),
  •  
  • // etc.
  •  
  • $.Deferred(function( deferred ){
  • $( deferred.resolve );
  • }).promise()
  •  
  • ).done( fn );

Great article as always. Cheers!

Reply to this Comment

While this technique is quite useful for some purposes (and it's much nicer syntax with deferreds than previously!), it's important for readers to understand that it doesn't fully fit the general script loader use-case.

There are 5 relevant dimensions:
1. multiple scripts
2. remote-domain scripts (thus, no non-CORS XHR)
3. loaded in parallel (performance)
4. execute in order (dependencies)
5. any script (un-wrapped)

Without using complex browser-dependent tricks (like LABjs and some other loaders do), you must choose at least one of those dimensions to give up.

In your code snippet above, you're giving up #4. You could instead get #4 by sacrificing #3 or #2 or #5. Etc.

If your use-case doesn't call for all 5, then you're safe using what's presented here. If you need to serve all 5 dimensions, you need a more complex dedicated script loader, like LABjs (or RequireJS, etc).

http://labjs.com

Reply to this Comment

@Eric,

Good point - and, it makes perfect sense! It's funny how it's sometimes so hard to transfer a concept into a slightly different context.

@Kyle,

This approach definitely doesn't support any kind of ordering; the only thing that it promises (no pun intended) is that the loads will finish before the then() handlers.

As for #5, I am not sure I understand what an un-wrapped script is?

I've never used a script-loader before, so this is basically my first dabblings. I had your LAB.js stuff open for so long in a tab, along with Require.js. Never got a chance to try it though (I think I didn't quite know how to start).

This is no fault of your own - you can see the trite example I came up with for this demo :)

Reply to this Comment

@Jason,

Sounds cool. How would you say that his Deferred implementation compares to jQuery's. jQuery is really the only one that I've looked into (other than way back when I took a peak at the Dojo Promise concept).

@Drew,

Ha ha, I am not sure about that. I haven't yet played with it; but, from what I've read, RequireJS does a lot of stuff.

Reply to this Comment

@Ben Boris uses the jquery 1.5 deferered object in his deferjs. He also has a jquery independant version of deferjs, DeferJS.js in his repository. I haven't looked but I assume its very simular to jQuery's in the independant version if not identical.

Reply to this Comment

@Jason,

Ah, gotcha. In either case, I have to say that I am enjoying deferred objects. When I read about them a long time ago, it made no sense. I thought to myself, "Why do something like this when $.ajax() already allows for callbacks." Now that I am playing around, though, it's really starting feel good.

Reply to this Comment

Post A Comment

?
You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.