Sunday, May 15, 2011

Using JQuery Deferred with Backbone

One of the most tedious tasks in Javascript is writing callbacks.
Ending up writing code like this is not unusual:
someRequest.execute(inputData,{
  success: function(result){
     someOtherRequest(result,{
       success: function(){
        // some other code here 
        },
       error: function() {
         alert('Some other error');
       }
     });
  },
  error: function(){
    alert("Some Error");
  }
});


Turning it into something like this would be nice:
someRequest.execute(inputData)
  .success(function(result){
             someOtherRequest(result)
             .success(function(){
                //some other code here
              });
             error(function(){
                alert('Some other error');
              });
      });
  .error(function(){
      alert("Some Error");
    });

Actually this is possible thanks to JQuery Deferred Object.
The idea is that whenever you need to execute some code that can trigger some other code once executed successfully, unsuccessfully or in both cases, instead of having to pass in callbacks, an object is returned and this object would provide methods to specify those callbacks.
The object I am talking about is called Promise and it represent a read-only proxy to the Deferred Object. It can be obtained by calling Deferred.promise()

Most important methods in Promise are:
  • done(function:Function) function will be called after the operation ends with success
  • fail(function:Function) function will be called after the operation ends with error
  • then(function:Function) function will be called after the operation with any status

All those methods are actually present also in Deferred which contains also the following ones:
  • resolve(args:Object) triggers calls to done() and then() passing args as input
  • reject(args:Object) triggers calls to fail() and then() passing args as input

Dealing with Promise instead of Deferred directly ensures that write operations to the task's result are not exposed: the result is therefore immutable.

Writing code involving callbacks becomes much easier in this way, as a simple example I propose here a variant of Backbone.sync:
Backbone.sync = function(method, model) {
    var type = methodMap[method];
    var modelJSON = (method === 'create' || method === 'update') ?
                    JSON.stringify(model.toJSON()) : null;

    // Default JSON-request options.
    var params = {
      url:          getUrl(model),
      type:         type,
      contentType:  'application/json',
      data:         modelJSON,
      dataType:     'json',
      processData:  false
    };

// I have removed the code handling emulateJSON and emulateHTTP to make this shorter

    // Make the request.
    return $.ajax(params);
  };

I have actually changed just 2 things from the original version:
  • success and error callbacks are not passed as input anymore
  • The Promise object from the call to $.ajax is forwarded back to the caller who can now attach success and error callbacks

Following steps would be to modify also Backbone.Model and Backbone.Collection accordingly.
I am not going to do that because the next release of Backbone will support Deferred.

This wanted to be just a simple demonstration of how code involving callbacks can become much easier to write and to read with Deferred Object. Other advantages include:
  • It is possible to register multiple callbacks instead of a single one.
  • It is possible to attach callbacks even after the operation has ended, this is because of the immutability of the Deferred result
  • With jQuery.when() synchronizing multiple asynchronous operations becomes extremelly easy

And that's all, I am now looking forward for the next version of Backbone and for playing a little bit more with Deferred(s) :)

2 comments:

  1. Hey,

    I am trying to create a website using javascript, backbone, mustache templates, jquery etc. I am new to all this (started with JS last week). Here is where I am stuck:

    When the url fragment changes, Backbone.Collection's url function is called which gets the data from the server-side at the provided url (I can check the call happens and JSON object is created), however, the jquery callback never gets executed.

    Any pointers would be very helpful.

    ~Jayz

    ReplyDelete
  2. Hi,

    How did you register the JQuery callback?
    Did you use the events property?

    Remember that the listener are available only for elements already present in the DOM!
    If you add an element manually after the initialize of your View has been called, you have to register the listener manually or to invoke delegateEvents..

    Diego

    ReplyDelete