Skip to content

Instantly share code, notes, and snippets.

@alano999
Created September 15, 2012 15:12
Show Gist options
  • Save alano999/3728411 to your computer and use it in GitHub Desktop.
Save alano999/3728411 to your computer and use it in GitHub Desktop.
Sequence (chain) multiple asynchronous jQuery operations, such as AJAX

MULTIPLE SEQUENTIAL ASYNCHRONOUS OPERATIONS IN JQUERY

Multiple asynchronous jQuery operations such as .ajax() and animations can be sequenced (chained) using the accompanying patterns which make use of jQuery Deferred techniques rather than the less extensible and non-ideal recursive call techniques.

There are three usage patterns, each implemented herein and tested using the supplied data for .ajax() transactions...

  1. Sequential in series using pre-defined data;
  2. Sequential in series, feeding each request using the response from previous request;
  3. Parallel request using pre-defined data, where responses are sequenced.

Typical uses

Typical examples of (1) might be a sequence of set animations that must be performed one after the other, or a set sequence of calls to external web services.

An example of (2) might be loading parts of a web page from web services where each part depends upon the data contained within the previous part.

Displaying several external web pages onto a single web page might be an example of scenario (3). The AJAX transactions to perform this could be performed serially one after the other as per scenario (1) but this would take longer. Consider that if ten page parts must be loaded and they each take around half a second to load, it would take 5 seconds to load them one after the other, whereas if they were loaded in parallel the overall operation would take only around half a second in total.

Cycling

Note that all my patterns continue to cycle regardless of whether an operation fails. Each pattern does provide a place where code can be written to optionally stop the sequence upon a failure, but in my experience this is not a very common requirement for multiple sequential asynchronous operations.

Parallel Sequencing

By default, JavaScript/jQuery perform .ajax() operations in parallel. If written simply one line after the other, AJAX operations will be kicked off straight away one after the other up to the limits of the client and server capabilities - often either 2 or 8 simultaneous AJAX transactions - when the situation is said to become "throttled" and further transactions are automatically fed through as soon as one of the connection sockets becomes free.

When sending 'n' AJAX requests in parallel, it cannot be assumed that the responses will be received in the same order - because responses will have varying lengths of data, the server may take differing times to construct the response, and usually errors will be reported back more quickly.

Sometimes the order of received responses does not matter and my special sequencing pattern will not be required. For example, if each of one hundred text fields on a web page need to be populated by .ajax() calls, then as long as the context of each .ajax() call is mapped to the relevant field then it doesn't actually matter in what order they are conducted.

If the responses from parallel requests do need to be reliably sequenced, then a developer's gut instinct is to fire off all transactions (let's say 100 transactions), and wait until they #all# complete. This is wasteful because if, say, the infrastructure only allows 8 simultaneous transactions, the "wait for all responses" algorithm would be waiting for around thirteen end-to-end request-response time periods before any responses are acted upon.

My special parallel sequencing pattern kicks off all requests straight away but doesn't wait for all to complete before starting to process the responses. My algorithm will process the responses as soon as they arrive back, but only in the required sequence.

"Requests" Object

My requests object allows callbacks to be declared at design time along with the request data and options, using a neat two-layer object pattern.

Each top layer object represents the data and options for each request such as the url, data, type, contentType, etc, and the allowed callbacks: success, error and complete. A "seq" object is nested into each request providing an extremely simple means to define the callbacks to be acted upon when the response has been received and sequenced - acting upon .done, .fail and .always once the response has been received and successfully sequenced. All callbacks within this model can be arrays of functions to allow multiple actions to be performed for each event type. The format is self-evident by looking at the example "requests" object included within this Gist.

requests.reduce(function (prev, req) {
return $.ajax(req).always(function () {
var a = arguments;
$.when(prev).always(function () { a[1] === 'success'
? $.Callbacks().add(req.seq.done).fireWith(req, a)
: $.Callbacks().add(req.seq.fail).fireWith(req, a);
$.Callbacks().add(req.seq.always).fireWith(req, a);
});
});
}, true);
/************************************************************
** NOTE #1:-
** This function block executes when the previous .ajax() succeeds
** Use the outer closure variables for the current .ajax() request thus:-
** req = current request's options
** idx = (zero-based) index of current request within array
** reqs = the entire array of request options
** Also use the response outcome from previous .ajax(), an array thus:-
** arguments[0] = data (the result of jQuery's conversion of xhr response)
** arguments[1] = textStatus (always 'success' at this point)
** arguments[2] = jQXHR
** Note that arguments array will be empty for the first .ajax() request
**
** NOTE #2:-
** This function block executes when the previous .ajax() fails
** The same variable as per NOTE #1 are available, but the array elements are different thus:-
** arguments[0] = jqXHR
** arguments[1] = textStatus (something like 'error')
** arguments[2] = errorThrown
**
** Example, that could be used in place of the comment "// NOTE #1" above:-
** req.data = arguments ? "" + arguments[0] + idx + "1" : "Test 0";
**
** The above example will, for the first request, set the request data to "Test 0", then
** for subsequent requests, set the request data to the value of the previous response
** followed by the array index position then the character "1". So, if the data returned by
** the second request was "hello", then the data sent to 3rd request (index = 2) will be "hello21"
************************************************************/
requests.reduce(function (prev, req, idx, reqs) {
return prev.pipe(function () {
// NOTE #1
return $.ajax(req);
}, function () {
// NOTE #2
return $.ajax(req);
});
}, $.Deferred().resolve() );
requests.reduce(function (prev, req) {
return prev.pipe(function () {
return $.ajax(req);
}, function () {
return $.ajax(req);
});
}, $.Deferred().resolve() );
/*******************************************************************************
** All my sequential patterns operate using pre-defined request data (the variable "requests").
** The object definition of "requests" is the test data used for all patterns.
**
** In the dynamic data pattern the data elemnt in "requests" object is ignored and
** overwritten but, obviously, it could also form part of the request data. For
** example, the example given could be modified to also add the pre-defined data
** onto end thus:-
**
** req.data = (arguments ? "" + arguments[0] + idx + "1" : "Test 0") + req.data;
*******************************************************************************/
var requests = [
{ url: '/echo/test/test1', type: 'POST', data: '250', complete: defComplete, seq: { always: seqAlways, done: seqDone, fail: seqFail} },
{ url: '/echo/test/test2', type: 'POST', data: '350', complete: defComplete, seq: { always: seqAlways, done: seqDone, fail: seqFail} },
{ url: '/echo/test/test3', type: 'POST', data: '40', complete: defComplete, seq: { always: seqAlways, done: seqDone, fail: seqFail} },
{ url: '/echo/test/test4', type: 'POST', data: '400', complete: defComplete, seq: { always: seqAlways, done: [seqDone, seqDone2]} },
{ url: '/echo/test/test5', type: 'POST', data: '300', complete: defComplete, seq: { always: [seqAlways, endIt]} }
];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment