Lazy loading asynchronous function with jQuery Deferreds


We are all well aware that out of process calls are very expensive, and a very common pattern, known as memoization, is used to “cache” the results, so a later call to the function returns a local result.
What do we do if that out of process call is asynchronous? For example an ajax request from a JavaScript application to a web service.
The problem with is that the first call to the function will fire off an asynchronous request.

var a = giveMeThingNumber(1);
console.log(a);

here the call to console.log may or not be occur before the function is ready. We must provide a callback to alleviate this situation:

var a = giveMeThingNumber(1,function(thing){
console.log(thing);
});

But the second call to this function is not asynchronous, and does not require this awkward callback. Additionally, we all have experienced the “callback” soup which comes about when we have to wait for a series of asynchronous calls to finish, before our “grand finale”.
Let’s see how to deal with all this.
I ran into this issue today while dealing with backbone.js. If anybody does not know by now, backbone.js is a framework which tries to propose a structure on JavaScript heavy code in think JavaScript clients (otherwise known as single page apps, or single page interfaces, SPA or SPI, respectively).
The basic idea, is that your data, instead of living in the DOM, or in hand rolled JavaScript objects, can be stored in special objects which have a common interface.
In this case the “Model” is an interface for single instances of your data model, and they live in “Collections”, which are sort of like a typed collections for your models to live in.
So in my application, I was dealing with events.

window.Event = Backbone.Model.extend({});
window.EventSet = Backbone.Collection.extend({
    model:Event
});

Once a collection is loaded with data, you can get an individual item using the “get” method:Event.get(1);
So what if the item is not in collection? I wanted to lazyload the item, if it doesn’t exist, and subsequent calls would just load straight from the collection.
This is exactly the issue I mentioned at the outset.
To the rescue, jQuery deferreds.
I’ll admit it, when jQuery 1.5 came out, and I read the obligatory “what’s new” blog posts, I didn’t get it at first. It took me a couple of times, and although I still don’t 100% “grok” it, I definitely have a decent understanding of it.
I mentioned before “callback” soup. Anybody who’s ever used node.js knows what i’m talking about. In node.js, just about every call is asynchronous, so anything depending on the result of the first caller, needs to be done in a callback function.
So too with asynchronous XHR calls. How many times have you seen this:

$.get('something',function(something){
      $.get('somethingElse',function(somethingElse) {
           console.log(something + somethingElse);
      }
});

As you can see, its neither fun to read or write, and can make your head spin around very quickly.
In jQuery 1.5, the ajax component was rewritten, and the concept of deferred was introduced. A deferred abstracts away the notion of asynchronousness, and normalizes the difference between asynchronous calls and synchronous calls. I encourage you to read the documentation to get a good idea.
The call to $.ajax, and family, returns now a Deferred object. This allows us to do things like:

function someAsynchronousCallIsDone() {
    return $.get('something');
}
$.when(someAsynchronousCallIsDone()).then(function(something) {
     console.log(something);
});

And if we have to wait for multiple asynchronous calls:

function getSomething() {
    return $.get('something');
}
function getSomethingElse() {
    return $.get('somethingElse');
}

$.when(getSomething(),getSomethingElse()).then(function(something,somethingElse) {
     console.log(something + somethingElse);
});

So basically I need to set up something like this:

$.when(Events.get(1)).then(function(model){doSomethingWithModel(model)});

I decided not to override get, not to mess up any unsuspecting callers, and instead made a new method lazyLoad:

lazyLoad: function(id) {
    var self = this;
    return this.get(id) || $.getJSON('/events/'+id,function(model){
       self.add(model);
    });
}

This works because part of the beauty of deferreds is that you can talk on callbacks whenever you want, and they’ll be executed, either when the call finishes, or right away if the call has already finished…a promise, in otherwords, that this callback will be executed.

var xhr = $.get('something');
$when(xhr).then(doSomethingWithResult);
doSomethingWhichMightHappenBeforeOrAfterNextCall()
$when(xhr).then(doSomethingElseWithResult);

The only problem here, is that we don’t want to return a standard JSON object, we want to return a Event object. After a bit of searching, I found that a jQuery Deferred has a method pipe, which allows me to do just that:

lazyLoad: function(id) {
    var self = this;
    return this.get(id) || $.getJSON('/events/'+id).pipe(function(result){
        self.add(result,{silent:true});
        return self.get(result.id);
    });
}
  1. #1 by Sultan at May 21st, 2012

    Great explanation with examples. Thank you very much!

    btw Tram.. I would guess that most developers are not going to visit posts like this in IE 7- or even IE for that matter.

(will not be published)
  1. No trackbacks yet.