Thursday, June 7, 2012

How can jQuery deferred be used?


jQuery 1.5 brings the new Deferred object and the attached methods .when , .Deferred and ._Deferred .



For those who havn't used .Deferred before I've annotated the source for it



What are the possible usages of these new methods, how do we go about fitting them into patterns?



I have already read the API and the source , so I know what it does. My question is how can we use these new features in everyday code?



I have a simple example of a buffer class that calls AJAX request in order. (Next one start after previous one finishes).




/* Class: Buffer
* methods: append
*
* Constructor: takes a function which will be the task handler to be called
*
* .append appends a task to the buffer. Buffer will only call a task when the
* previous task has finished
*/
var Buffer = function(handler) {
var tasks = [];
// empty resolved deferred object
var deferred = $.when();

// handle the next object
function handleNextTask() {
// if the current deferred task has resolved and there are more tasks
if (deferred.isResolved() && tasks.length > 0) {
// grab a task
var task = tasks.shift();
// set the deferred to be deferred returned from the handler
deferred = handler(task);
// if its not a deferred object then set it to be an empty deferred object
if (!(deferred && deferred.promise)) {
deferred = $.when();
}
// if we have tasks left then handle the next one when the current one
// is done.
if (tasks.length > 0) {
deferred.done(handleNextTask);
}
}
}

// appends a task.
this.append = function(task) {
// add to the array
tasks.push(task);
// handle the next task
handleNextTask();
};
};



I'm looking for demonstrations and possible uses of .Deferred and .when .



It would also be lovely to see examples of ._Deferred .



Linking to the new jQuery.ajax source for examples is cheating.



Bounty: Show us what techniques are available when we abstract away whether an operation is synchronously or asynchronously done.


Source: Tips4all

7 comments:

  1. The best use case I can think of is in caching AJAX responses. Here's a modified example from Rebecca Murphey's intro post on the topic:

    var cache = {};

    function getData( val ){

    // return either the cached value or an
    // jqXHR object (which contains a promise)
    return cache[ val ] || $.ajax('/foo/', {
    data: { value: val },
    dataType: 'json',
    success: function( resp ){
    cache[ val ] = resp;
    }
    });
    }

    $.when(getData('foo')).then(function(resp){
    // do something with the response, which may
    // or may not have been retreived using an
    // XHR request.
    });


    Basically, if the value has already been requested once before it's returned immediately from the cache. Otherwise, an AJAX request fetches the data and adds it to the cache. The $.when/.then doesn't care about any of this; all you need to be concerned about is using the response, which is passed to the .then() handler in both cases.

    Deferreds are perfect for when the task may or may not operate asynchronously, and you want to abstract that condition out of the code.

    Another real world example using the $.when helper:

    $.when( $.getJSON('/some/data/'), $.get('template.tpl') ).then(function( data, tmpl ){

    $( tmpl ) // create a jQuery object out of the template
    .tmpl( data) // compile it
    .appendTo( "#target" ); // insert it into the DOM

    });

    ReplyDelete
  2. Another use that I've been putting to good purpose is fetching data from multiple sources. In the example below, I'm fetching multiple, independent JSON schema objects used in an existing application for validation between a client and a REST server. In this case, I don't want the browser-side application to start loading data before it has all the schemas loaded. $.when.apply().then() is perfect for this. Thank to Raynos for pointers on using then(fn1, fn2) to monitor for error conditions.

    fetch_sources = function(schema_urls) {
    var fetch_one = function(url) {
    return $.ajax({
    url: url,
    data: {},
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    });
    }
    return $.map(schema_urls, fetch_one);
    }

    var promises = fetch_sources(data['schemas']);
    $.when.apply(null, promises).then(
    function() {
    var schemas = $.map(arguments, function(a) { return a[0] });
    start_application(schemas);
    },
    function() {
    console.log("FAIL", this, arguments);
    });

    ReplyDelete
  3. Here is a slightly different implementation of an AJAX cache as in ehynd's answer.

    As noted in fortuneRice's follow-up question, ehynd's implementation didn't actually prevent multiple identical requests if the requests were performed before one of them had returned. That is,

    for (var i=0; i<3; i++) {
    getData("xxx");
    }


    will most likely result in 3 AJAX requests if the result for "xxx" has not already been cached before.

    This can be solved by caching the request's Deferreds instead of the result:

    var cache = {};

    function getData( val ){

    // Return a promise from the cache (if available)
    // or create a new one (a jqXHR object) and store it in the cache.
    var promise = cache[val];
    if (!promise) {
    promise = $.ajax('/foo/', {
    data: { value: val },
    dataType: 'json'
    });
    cache[val] = promise;
    }
    return promise;
    }

    $.when(getData('foo')).then(function(resp){
    // do something with the response, which may
    // or may not have been retreived using an
    // XHR request.
    });

    ReplyDelete
  4. A deferred can be used in place of a mutex. This is essentially the same as the multiple ajax usage scenarios.

    MUTEX

    var mutex = 2;

    setTimeout(function() {
    callback();
    }, 800);

    setTimeout(function() {
    callback();
    }, 500);

    function callback() {
    if (--mutex === 0) {
    //run code
    }
    }


    DEFERRED

    function timeout(x) {
    var dfd = jQuery.Deferred();
    setTimeout(function() {
    dfd.resolve();
    }, x);
    return dfd.promise();
    }

    jQuery.when(
    timeout(800), timeout(500)).done(function() {
    // run code
    });


    When using a Deferred as a mutex only, watch out for performance impacts (http://jsperf.com/deferred-vs-mutex/2). Though the convenience, as well as additional benefits supplied by a Deferred is well worth it, and in actual (user driven event based) usage the performance impact should not be noticeable.

    ReplyDelete
  5. Another example using Deferreds to implement a cache for any kind of computation (typically some performance-intensive or long-running tasks):

    var ResultsCache = function(computationFunction, cacheKeyGenerator) {
    this._cache = {};
    this._computationFunction = computationFunction;
    if (cacheKeyGenerator)
    this._cacheKeyGenerator = cacheKeyGenerator;
    };

    ResultsCache.prototype.compute = function() {
    // try to retrieve computation from cache
    var cacheKey = this._cacheKeyGenerator.apply(this, arguments);
    var promise = this._cache[cacheKey];

    // if not yet cached: start computation and store promise in cache
    if (!promise) {
    var deferred = $.Deferred();
    promise = deferred.promise();
    this._cache[cacheKey] = promise;

    // perform the computation
    var args = Array.prototype.slice.call(arguments);
    args.push(deferred.resolve);
    this._computationFunction.apply(null, args);
    }

    return promise;
    };

    // Default cache key generator (works with Booleans, Strings, Numbers and Dates)
    // You will need to create your own key generator if you work with Arrays etc.
    ResultsCache.prototype._cacheKeyGenerator = function(args) {
    return Array.prototype.slice.call(arguments).join("|");
    };


    Here is an example of using this class to perform some (simulated heavy) calculation:

    // The addingMachine will add two numbers
    var addingMachine = new ResultsCache(function(a, b, resultHandler) {
    console.log("Performing computation: adding " + a + " and " + b);
    // simulate rather long calculation time by using a 1s timeout
    setTimeout(function() {
    var result = a + b;
    resultHandler(result);
    }, 1000);
    });

    addingMachine.compute(2, 4).then(function(result) {
    console.log("result: " + result);
    });

    addingMachine.compute(1, 1).then(function(result) {
    console.log("result: " + result);
    });

    // cached result will be used
    addingMachine.compute(2, 4).then(function(result) {
    console.log("result: " + result);
    });


    The same underlying cache could be used to cache Ajax requests:

    var ajaxCache = new ResultsCache(function(id, resultHandler) {
    console.log("Performing Ajax request for id '" + id + "'");
    $.getJSON('http://jsfiddle.net/echo/jsonp/?callback=?', {value: id}, function(data) {
    resultHandler(data.value);
    });
    });

    ajaxCache.compute("anID").then(function(result) {
    console.log("result: " + result);
    });

    ajaxCache.compute("anotherID").then(function(result) {
    console.log("result: " + result);
    });

    // cached result will be used
    ajaxCache.compute("anID").then(function(result) {
    console.log("result: " + result);
    });


    You can play with the above code in this jsFiddle.

    ReplyDelete
  6. You can use a deferred object to make a fluid design that works well in webkit browsers. Webkit browsers will fire resize event for each pixel the window is resized, unlike FF and IE which fire the event only once for each resize. As a result, you have no control over the order in which the functions bound to your window resize event will execute. Something like this solves the problem:

    var resizeQueue = new $.Deferred(); //new is optional but it sure is descriptive
    resizeQueue.resolve();

    function resizeAlgorithm() {
    //some resize code here
    }

    $(window).resize(function() {
    resizeQueue.done(resizeAlgorithm);
    });


    This will serialize the execution of your code so that it executes as you intended it to. Beware of pitfalls when passing object methods as callbacks to a deferred. Once such method is executed as a callback to deferred, the 'this' reference will be overwritten with reference to the deferred object and will no longer refer to the object the method belongs to.

    ReplyDelete
  7. You can also integrate it with any 3rd party libraries who make use of JQuery.
    One of those is Backbone, who is actually going to support Deferred in their next version.
    I have talked about it also on my blog http://a-developer-life.blogspot.com/2011/05/using-jquery-deferred-with-backbone.html

    ReplyDelete