Deferred object as callback

Posted October 10th, 2012 by guidone with 6 Comments

Callbacks are a nice way of Iversion of control, but are even more powerful with deferred objects.
Basically callbacks are a way to pass back the control from a called function, the deferred/promise just do the opposite: it returns the control the called function.

Consider this scenario: a table widget, with the classic delete/modify buttons next to each row. Since the table widget is a generic component, we want to handle the delete operation on the model outside the widget, and we don’t event want to extend the generic object to do that, we want to user a callback:

$.widget('my_table_widget',{
   onDelete: function(id_of_row) {
      var that = $(this);
      // call the backend and delete the record
      $.ajax({
         method: 'delete',
         url: '/api/records/'+id_of_row
         })
         .done(function() {
            // ok, record delete, now I should update the table
            that.widget('remove_row',id_of_row);
            });
         }
      });

In the example above: the callback first calls the backend to remove the record and then removes the row from the table (I mean the DOM). In order to do this: it needs to know something about the table widget (in this case, the method “remove_row”).
But what if we could pass the ball back to the widget? Let’s rewrite it with a deferred object:

$.widget('my_table_widget',{
   onDelete: function(id_of_row) {
      var deferred = $.Deferred();
      // call the backend and delete the record
      if (confirm('Delete?')) {
         $.ajax({
            method: 'delete',
            url: '/api/records/'+id_of_row
            })
            .done(function() {
               // ok, record delete, now I should update the table
               deferred.resolve();
               })
            .fail(function() {
               deferred.reject();
               })
         }
      else {
         deferred.reject();
         } 
      return deferred.promise();
      }
   });

It’s more linear but, most important, the callback knows nothing about the remove_that_damn_row_from_the_dom method of the widget, it just passed back the control, it’s like saying “I’m done, it’s your turn”.
More separation, less documentation to read, easier to implement, less errors.

On the widget side, the callback should be treated this way

// somewhere inside the table widget, here is where we execute the callback
var callback_result = options.onDelete.call(widget,id);
// if the callback answer with a deferred/promise, this will
// handle it when it's resolved/rejected
if (isPromise(callback_result)) {
   callback_result
      .done(function() {
         // ok, asynch operation on the other side is over
         // remove the row from the dome
         widget.widget('remove_row',id);
         })
      .fail(function() {
         // do nothing
         })
   }

Pitfalls of deferred/promise pattern

Posted April 17th, 2012 by guidone with No Comments

The Deferred/Promise pattern is great for two reason:

  • It decouples the when from the then statements: basically the operations that need to be fulfilled are completely separated from the code that will be executed once completed
  • The when instructions could be synchronous or asynchronous, it’s possible switch to an asynchronous operation in the $.when() fuction without changing anything else

So far so good, but we need to keep in mind that this pattern it’s just an abstraction and we should be always aware of where, in the code, our procedure is executed.

Consider this example where a popover is used to pick up some options: the whole process is handled with a deferred() object:

function openPopover() {
   var deferred = $.Deferred();
   $('#my_popover')
      // some instructions to handle the
      // popover
      .click(function() {
         // hide the popover
         $(this).hide();
         // resolve the promise
         deferred.resolve('my_choice');
         return false;
         })
   return deferred.promise();
   }

And the code to trigger the popover is

$.when(openPopover())
   .then(function() {  
      very_long_operation();
      });

What is not clear here is that the code of the function very_long_operation() is actually executed within the handler of the .click(), after the .hide() operation but before the click handler is completed.
Here is the point: if the function very_long_operation() turns out to be a long process, say 1 or 2 secs, it will take this interval for the popover to disappear after the click, since the DOM will be updated only at the end of the event handler.
From the user perspective, this results in a less responsive user interface.

This is particulary bad since we want to decouple the when from then: openPopover() doesn’t know anything about very_long_operation(), nevertheless wether is a short or long process.

The smartest solution could be a WebWorker: refactor each lines of very_long_operation() in order to exploit HTML5, but this will not be available on oldest browsers.

The quickest solution is and old-quick-and-dirt setTimeout:

$.when(openPopover())
   .then(function() {  
      setTimeout(very_long_operation,1);
      });

It just detach the long process very_long_operation() from the event loop, letting the click handler to complete (and update the DOM), and everything is safe: the user interface and our decoupled code.

The same code using Stackable:

$.when(openPopover())
   .then(function() {  
      $function(very_long_operation).fork();
      });

PS: The same result could be achieved by wrapping with the timeout trick the line deferred.resolve(‘my_choice’); but this is wrong from the point of view of code decoupling: the popover code knows nothing (and doesn’t have to) about the code executed with the resolve() statement, putting the timeout here would imply that our deferred object always assumes to deal with very long process.
Deferreds is all about decoupling

  • Categories

  • Tags

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 2 other subscribers

  • Twitter

    Flickr Stream