Using Deferred Objects As An Asynchronous Script Loader In jQuery 1.5
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.
Want to use code from this post? Check out the license.
Reader Comments
Hey Ben,
I noticed it's possible to return a promise with the
signature simply by chaining a
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:
Great article as always. Cheers!
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
@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 :)
Thanks for the article Ben, I was looking over Boris Moore's Deferjs yesterday:
https://github.com/BorisMoore/DeferJS
He also uses deferered objects for his Script loader...
So can RequireJS be replaced by a much thinner jQuery plugin now? /hint
@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.
@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.
@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.
Hello, I'm a bit late to the party but… as described here [*] $.getScript seems to be skipping the cache thus loading the file from the server every time it's requested (perhaps at every page load).
I'd suggest using YepNope.js instead, it's < 2KB
[*] http://www.kevinleary.net/load-external-javascript-jquery-getscript/
Actually there's a better solution here (also check the comments), you can disable the cache of getScript: http://jamiethompson.co.uk/web/2008/07/21/jquerygetscript-does-not-cache/