Using Deferred Objects As An Asynchronous Script Loader In jQuery 1.5

Posted February 12, 2011 at 2:15 PM by Ben Nadel

Tags: ColdFusion, Javascript / DHTML

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

Feb 12, 2011 at 2:58 PM // reply »
10 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!


Feb 12, 2011 at 3:17 PM // reply »
14 Comments

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


Feb 12, 2011 at 4:34 PM // reply »
11,238 Comments

@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 :)


Feb 12, 2011 at 9:00 PM // reply »
2 Comments

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...


Feb 13, 2011 at 3:55 PM // reply »
54 Comments

So can RequireJS be replaced by a much thinner jQuery plugin now? /hint


Feb 14, 2011 at 4:21 PM // reply »
11,238 Comments

@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.


Feb 14, 2011 at 4:28 PM // reply »
2 Comments

@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.


Feb 14, 2011 at 4:40 PM // reply »
11,238 Comments

@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.


Nov 30, 2011 at 6:30 PM // reply »
2 Comments

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/


Nov 30, 2011 at 6:37 PM // reply »
2 Comments

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/


Post A Comment

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.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 17, 2013 at 7:42 PM
HashKeyCopier - An AngularJS Utility Class For Merging Cached And Live Data
Ben - thanks so much for posting these Angular articles and findings, they've been a huge help towards learning one of the more 'complex' JavaScript frameworks out there (IMO). I have been using Angu ... read »
May 16, 2013 at 5:01 PM
UPDATE: Parsing CSV Data Files In ColdFusion With csvToArray()
Your code was the closest thing I've found to obtaining some direction for converting ISO fields to values that CF can translate properly. Thank you for posting! ... read »
May 15, 2013 at 10:37 PM
Very Simple Pusher And ColdFusion Powered Chat
hi id making plz easy ... read »
May 15, 2013 at 6:07 PM
Making SOAP Web Service Requests With ColdFusion And CFHTTP
Ben, you once again saved my bacon at work. Thank you, thank you, thank you! ... read »
May 15, 2013 at 4:15 PM
What If All User Interface (UI) Data Came In Reports?
@Josh, Thanks! @Ben, I definitely recommend the David West book "Object Thinking" I've been quoting from. It goes deeply into the philosophy and history of OO programming. His breadth ... read »
May 15, 2013 at 11:36 AM
Ask Ben: Print Part Of A Web Page With jQuery
I found this helpfull when you need to keep (refresh) the original parent page after closing the iframe child print dialog (Hoping you're not using a form at this time so it won't submit again): On ... read »
May 14, 2013 at 7:13 PM
What If All User Interface (UI) Data Came In Reports?
@Jonah, If there's any books you'd recommend on the subject of domain modelling, I'd love to hear it. I just downloaded the free PDF of "Domain Driven Design Quickly". Figured I'd give it ... read »
May 14, 2013 at 6:57 PM
The UX Of Prototyping: Low-Fidelity Is The New High-Fidelity
@Phillip, I'm not sure I follow what you mean? Are you saying that you looked at the list of widgets provided by the jQuery UI and let that be your style guide? ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools