jQuery.whenSync() Plugin For Chaining Asynchronous Callbacks Using Deferred Objects
Last week, I started to talk about chaining asynchronous Validation rules using jQuery Deferred objects. After writing up that post, I thought I might be able to factor-out the core idea into its own jQuery plugin - jQuery.whenSync(). Like the native jQuery.when() method, the jQuery.whenSync() method takes a variable-number of arguments; however, unlike the when() method, the whenSync() method accepts callbacks, not Deferred objects. This is because the whenSync() method encapsulates the creation of Deferred objects for each one of the asynchronous callback invocations. Each callback will be invoked in-order as the previous callback is resolved. If any of the callbacks is rejected, the entire chain of callbacks is rejected.
Before we look at the plugin code, let's take a look at how it can be used. The following is a trivial example; but, it demonstrates the mechanics of the workflow. Each callback is invoked in-order, given a Deferred instance and all previously-resolved results.
<!DOCTYPE html>
<html>
<head>
<title>jQuery.whenSync() For Asynchronous, Deferred Chaining</title>
<!-- Include jQuery and the whenSync() plugin. -->
<script type="text/javascript" src="../jquery-1.7.1.js"></script>
<script type="text/javascript" src="./jquery.whensync.js"></script>
<script type="text/javascript">
// Get a reference to the core Array slice() method for the
// debugging of our ongoing results.
var slice = Array.prototype.slice;
// Create a utility function that allows us to resolve the
// given deferred object after the given amount of time.
var resolver = function( deferred, result, timeout ){
// Resolve the deferred in the future.
setTimeout(
function(){
// Resolve the given deferred.
deferred.resolve( result );
},
timeout
);
};
// -------------------------------------------------- //
// -------------------------------------------------- //
// Serialize a chain of asynchronous callsbacks. Each one of
// these callbacks will receive a Deferred object so that it
// can tell the whenSync() method when to move onto the next
// asynchronous method in the chain.
var asyncChain = $.whenSync(
// Asynchronous method.
function( deferred ){
// Log the current method context.
console.log( "Method 1" );
console.log( ">Results:", slice.call( arguments, 1 ) );
// Reolve this callback (shortly).
resolver( deferred, "result1", 500 );
},
// Asynchronous method.
function( deferred, result1 ){
// Log the current method context.
console.log( "Method 2" );
console.log( ">Results:", slice.call( arguments, 1 ) );
// Reolve this callback (shortly).
resolver( deferred, "result2", 1000 );
},
// Asynchronous method.
function( deferred, result1, result2 ){
// Log the current method context.
console.log( "Method 3" );
console.log( ">Results:", slice.call( arguments, 1 ) );
// Reolve this callback (shortly).
resolver( deferred, "result3", 1500 );
}
);
// Bind to the asynchronous chain.
asyncChain.done(
function( result1, result2, result3 ){
// Log out all the results.
console.log( "Done() Binding" );
console.log( ">Results:", arguments );
}
);
</script>
</head>
<body>
<!-- Left intentionally blank. -->
</body>
</html>
As you can see, we have a synchronous chain of three asynchronous methods. Each method logs out the results that are passed into it. Each method also resolves itself after some delay (500, 1000, and 1500 milliseconds). When we run the above code, we get the following console output:
Method 1
>Results: []
Method 2
>Results: ["result1"]
Method 3
>Results: ["result1", "result2"]
Done() Binding
>Results: ["result1", "result2", "result3"]
As you can see, the results of the previously resolved methods are passed-through to each subsequent method. Then, when the chain has resolved completely, all results are passed, in-order, to the bound done() handler.
Ok, now let's take a look at the actual jQuery plugin code:
jquery.whensync.js - Our whenSync() jQuery Plugin
// Define a sandbox in which the whenSync() plugin can be defined.
(function( $ ){
// Define the whenSync() jQuery plugin. This plugin is designed
// to take N-number of callbacks. Each callback will be invoked
// in order, given a Deferred object as its first invocation
// argument.
//
// callback( Deferred [, result1, result2, resultN] );
//
// Additionally, all previous results will be passed as arguments
// 2-N of the callback. Subsequent callbacks will not be invoked
// until the Deferred object is resolved.
$.whenSync = function( /* callbacks */ ){
// Create a master deferred object for the entire validation
// process. This will be rejected if ANY of the callback
// Deferred objects is rejected. It will be resolved only
// after ALL of the callback Deferreds are resolved.
var masterDeferred = $.Deferred();
// Create an array to hold the master results. As each
// callback is invoked, we are going to pass-through the
// aggregate of all the previous results.
var masterResults = [];
// Create a true array of callback functions (so that we
// can make use of the core Array functions).
var callbacks = Array.prototype.slice.call( arguments );
// Check to make sure there is at least one callback. If there
// are none, then just return a resolved Deferred.
if (!callbacks.length){
// Nothing more to do - resolve the master result.
masterDeferred.resolve()
// Return the promise of the result.
return( masterDeferred.promise() );
}
// I provide a recursive means to invoke each callback.
// I take the given callback to invoke. This callback will be
// invoked with the previously resolved master Results.
var invokeCallback = function( callback ){
// Create a deferred result for this particular callback.
var deferred = $.Deferred();
// Create a promise for our deferred object so that we
// can properly bind to the resolve / reject handlers
// for the synchronous callback step.
deferred.promise().then(
function( /* Resolve arguments. */ ){
// Take the current results and add them to the
// end of the master results.
masterResults = masterResults.concat(
Array.prototype.slice.call( arguments )
);
// This callback was resolved. Now, let's see if
// we have another callback to execute.
var nextCallback = callbacks.shift();
// Check for a next callback.
if (nextCallback){
// Recusively invoke the callback.
return( invokeCallback( nextCallback ) );
}
// No more callbacks are available - our chain of
// callbacks is complete. We can therefore
// consider the entire chain to be resolved. As
// such, we can resulve the master deferred.
masterDeferred.resolve.apply(
masterDeferred,
masterResults
);
},
function( /* Reject arguments */ ){
// This callback was rejected. We cannot proceed
// with any more steps in callback chain - we must
// reject the master deferred.
// Reject the master deferred and pass-through the
// rejected results.
masterDeferred.reject.apply(
masterDeferred,
arguments
);
}
);
// While the callback is intended to be asynchronous,
// let's catch any synchronous errors that happen in the
// immediate execution space.
try {
// Create an invocation arguments collection so that
// we can seamlessly pass-through any previously-
// resolved result. The Deferred result will always
// be the first argument in this argument collection.
var callbackArguments = [ deferred ].concat( masterResults );
// Call the callback with the given arguments (the
// Deferred result and any previous results).
callback.apply( window, callbackArguments );
} catch( syncError ){
// If there was a synchronous error in the callback
// that was not caught, let's return the native error.
masterDeferred.reject( syncError );
}
};
/* END: invokeCallback(){ .. } */
// Invoke the first callback.
invokeCallback( callbacks.shift() );
// Return the promise of the master deferred object.
return( masterDeferred.promise() );
};
})( jQuery );
// End jQuery plugin.
Right now, the jQuery.whenSync() method only deals with the Deferred objects that it has created internally. An interesting next step would be to augment the underlying invocation mechanism to accept a Deferred object as the return from the callback invocation. This way, a given callback could override the Deferred object being used to signal the outcome of the asynchronous operation. This would be useful for passing-through AJAX requests without an intermediary results binding.
Want to use code from this post? Check out the license.
Reader Comments
I've read this twice (ok, skimmed it ;) and I don't get how to actually use this still. Your example just isn't clicking with me. Would you mind writing another example? Here is a specific use case. I want to do a network call, let's say something simple:
$.get("http://www.foo.com")
I want to do that N times, and have your plugin ensure than call 2 happens after call 1. (And yeah, I know calling the same URL N times is done, normally it would be dynamic.) Heck, let's make it dynamic
for(var i=0;i<10; i++) {
$.get("http://www.foo.com/?x="+i, {}, function(res, code) {
//handle result
});
}
Right now, that code would be async. How would you modify that to be asynch but chained?
@Ray,
To be fair, when I wrote this code, I was thinking a lot of Node.js, where I was more concerned with chaining server-side validation steps. For example, in Node.js, going to the database is an asynchronous action. So, if I wanted to get a User record from the DB in order to confirm something, I'd have to do it asynchronously.
That said, I'm not so great a Node.js, so I "practice" a lot of JavaScript in jQuery :D
Right now, the way the whenSync() function works is that it takes N number of functions and invokes each of them in turn, passing in a Deferred object (that *YOU* have to resolve or reject):
So, if you wanted to chain your $.get() functions, you'd have to wrap them up:
Honestly, this feels a bit annoying. I'd rather just pass the $.get() response directly to the whenSync() method, in the way that when() works.
I'd like to augment the whenSync() method to accept *either* a Function OR a Deferred object. This way, you could use asynchronous processing but still move down the chain in serial.
Not sure if that clears anything up :)
A bit... does your plugin allow you to pass in an array? It looked like it was inline.
@Ray,
No, currently only inline functions; though that would be a nice enhancement. I've put it on the list :)
Just used this successfully, thanks.
My use case:
I have a jQuery dialog with 3 related select boxes. Changing the value of the first causes a getJSON request to change the options of the second, and changing the second box refreshes the choices of the third. After making the selections, the data is saved and appears in a table on the page containing the dialog.
I want the user to be able to edit an existing entry, so I need to pass the existing values to a function which will populate the select boxes correctly and then select the appropriate values.
Because the options available in the second box are dependent on what is selected in the first box, and the third from the second, this requires some careful choreography. I can't set the value in the second box until it has been repopulated from the value set in the first box, etc.
Your plugin made this a bit easier than the way I had been doing it.