Reputation: 43
I need to populate an HTML table using records retrieved via a series of AJAX requests. The AJAX requests are all generated in a while loop. The total number of requests made is governed by a value provided by the server in a previous exchange of information. The AJAX requests need to be handled asynchronously while the results of the request need to be processed in the order in which originally requested.
The problem, of course is that the responses to the requests do not always get answered in order. So, after reading up on jquery's Deferred/promise interface I thought I had a solution in hand. But for the life of me can't get my head around how to defer the processing (populating the table) of the results of a requests until the results of previous request(s) have been hanlded. I've found many examples on StackOverflow that get me close, but I can't seem to connect the dots to get it all working in my application.
First I tried using an array of Deferred objects, thinking that I could use indexed references to them to make the processing of one set of records dependent upon the completion of the processing of the previous set (in "as requested" order) set of data. But I couldn't figure out how to create a promise/Deferred object associated with the actual processing of the data - using .then().
var deferreds = [];
var recsPerPkt = 20;
var recRqstd = 0;
while(recsRqstd < totalRecsAvailable) {
console.log("Next record index to request: " + nextNdxRqst)
// Collect an array of the "promise" objects that $.ajax() returns for each call.
deferreds.push( $.ajax({
url: 'eventSummaryData',
type: 'get',
cache: false,
data: {StartNdxNum: nextNdxRqst, NumRecords: recsPerPkt}
}) // End $.ajax({ url: 'trainSummaryData', ...
); // End deferreds.push()
recsRqstd += recsPerPkt;
nextNdxRqst = recsRqstd;
if (deferreds.length > 1)
{
deferreds[deferreds.length - 2].then(
function (jsonData) {
if (jsonData.ok) {
// Now display the rows/records included in this packet.
displayrRecordsInTable({"rows": jsonData.rows});
}
},
function(){
$('#error-msg').text('HTTP error: ' + errorThrown);
}
);
}
}
$.when.apply(null, deferreds).then( function(){
console.log("Processing of AJAX'd data complete. ")
configureTableControls();
});
Then I found a "pattern" that chained the processing functionality using then(), but the example didn't quite fit my exact situation and ended up having no reference to the http response data in my process-the-data handler. When I run this, the browser logs to the console, "jsonData undefined" .
var prevPromise = $.Deferred().resolve();
var recsPerPkt = 20;
var recRqstd = 0;
while(recsRqstd < totalRecsAvailable) {
prevPromise = prevPromise.then(function(){
return $.ajax({
url: 'eventSummaryData',
type: 'get',
cache: false,
data: {StartNdxNum: nextNdxRqst, NumRecords: recsPerPkt}
});
}).then(function (jsonData) {
if (jsonData.ok) {
// Now display the rows/records included in this packet.
displayTrainSummaryRows({"rows": jsonData.rows});
}
},
function(){
$('#error-msg').text('HTTP error: ' + errorThrown);
}
);
recsRqstd += recsPerPkt;
nextNdxRqst = recsRqstd;
}
So, how can I enforce the processing of AJAX requested data in the sequence in which the requests were originally made? Thanks in advance.
Upvotes: 4
Views: 3094
Reputation: 11269
I would suggest using native Promises over jQuery's deferred objects. They are much more readable, easier to understand, native to the language, and have increasing browser support (with the exception of all IE versions). To account for browser support, use a polyfill like es6-promise and you'll be set. This article does a good job of explaining the basics of promises.
Check out the section of that article I linked to, titled "Parallelism and sequencing" because that really goes in depth on how this works. Essentially you need to create a promise generator function, some sort of mapping for how many ajax requests you need to make (in this case just an array that goes up by 20 each time), and a sequence
variable that holds the previous promise that was looped through.
var totalRecsAvailable = 10; //generated elsewhere apparently
var recsPerPkt = 20;
var nextNdxRqst = 0;
var recordRanges = [];
var sequence = Promise.resolve(); //initialize to empty resolved promise
//this generates a promise
function getMyData(startPosition) {
return new Promise(function(resolve,reject){
$.ajax({
type: 'GET',
dataType: 'json',
url: 'eventSummaryData',
data: {StartNdxNum: startPosition, NumRecords: recsPerPkt}
success: function(response){resolve(response);},
error: function(response){reject(response);}
});
});
}
//build out array to inform our promises what records to pull & in which order
for (var i = 0; i < totalRecsAvailable; i++) {
recordRanges.push(nextNdxRqst);
nextNdxRqst += recsPerPkt;
}
//loop through record ranges, chain promises for each one
recordRanges.forEach(function(range) {
sequence = sequence.then(function() {
return getMyData(range); // return a new Promise
}).then(function(data) {
//do stuff with the data
addToHtmlTable(data.something);
}).catch(function(error) {
//something went wrong
console.log(error);
});
});
As outlined in that article, using reduce instead of a forEach
is actually a bit better, but I thought this was more clear what was happening.
For slightly faster performance, you should use Promise.all(). This takes an iterable (like an array) of promises, runs those promises asynchronously, and then saves the results to an array in the order they were passed. If one of the promises fails, the whole thing will fail and give an error. This sounds exactly like what you need. For example, you could do something like this:
var recsPerPkt = 20;
var nextNdxRqst = 0;
var totalRecsAvailable = 10; //generated elsewhere apparently
var promises = [];
//this generates a promise
function getMyData(startPosition, recordsNumber) {
return new Promise(function(resolve,reject){
$.ajax({
type: 'GET',
dataType: 'json',
url: 'eventSummaryData',
data: {StartNdxNum: startPosition, NumRecords: recordsNumber}
success: function(response){resolve(response);},
error: function(response){reject(response);}
});
});
}
//create list of promises
for (var i = 0; i < totalRecsAvailable; i++) {
promises.push(getMyData(nextNdxRqst,recsPerPkt));
nextNdxRqst += recsPerPkt;
}
//This will run once all async operations have successfully finished
Promise.all(promises).then(
function(data){
//everything successful, handle data here
//data is array of results IN ORDER they were passed
buildTable(data);
},
function(data){
//something failed, handle error here
logoutError(data);
}
);
That should set you down the right path.
Upvotes: 2
Reputation: 3302
I don't know of any jQuery function that does what you want, but here is a function processInOrder
that will not perform your callback until all previous async ops have resolved while still allowing you to access their results
function processInOrder(arr, cb){
if( arr.length > 0 ){
arr[0].then(function(result){
cb(result);
processInOrder(arr.slice(1), cb);
});
}
}
var deferreds = [];
for(var i=0; i<4; i++){
deferreds.push( asyncRequest(i) );
}
processInOrder(deferreds, display);
Note that while I'm not positive, I'm fairly sure that this form of recursion does not nuke the call stack for large numbers of requests.
Here it is in a jsFiddle
Upvotes: 1