In my JS, I need to get the contents of 3 files with AJAX, then do some code. This has led to a rather strange looking creation of nested async functions. Also anytime I\'m
Here are several different techniques with and without the use of deferreds. In all cases, all the ajax calls are launched and then some piece of code keeps track of when all the ajax calls have completed and collects the data from the calls as they complete so when the last one completes all the data is available.
You can launch all three ajax calls at once and just check in each completion function if they are all done yet, storing the results in a local variable until they are all done:
function loadFilesAndDoStuff() {
var cntr = 3;
var data1, data2, data3;
function checkDone() {
--cntr;
if (cntr === 0) {
// all three are done here
someOtherFunction(combined_file_data);
}
}
$.get(firstFile, function(data) {
data1 = data;
checkDone();
});
$.get(secondFile, function(data) {
data2 = data;
checkDone();
});
$.get(thirdFile, function(data) {
data3 = data;
checkDone();
});
}
Or, you can put more of it in a common function and pass the array of filenames to the function:
function loadFilesAndDoStuff(filesArray) {
var results = [];
var doneCnt = 0;
function checkDone(index, data) {
results[index] = data;
++doneCnt;
if (doneCnt === filesArray.length) {
// all results are in the results array now
}
}
for (var i = 0; i < filesArray.length; i++) {
results.push(null);
$.get(filesArray[i], checkDone.bind(this, i));
}
}
Using Deferreds, you can do this:
function loadFilesAndDoStuff(filesArray) {
var results = [];
var deferreds = [];
function doneOne(index, data) {
results[index] = data;
}
for (var i = 0; i < filesArray.length; i++) {
results.push(null);
deferreds.push($.get(filesArray[i], doneOne.bind(this, i)));
}
$.when.apply($, deferreds).done(function() {
// all ajax calls are done and results are available now
});
}
Or, an even shorter version using the fact that deferreds save the arguments from the sucess handlers for each deferred:
function loadFilesAndDoStuff(filesArray) {
var deferreds = [];
for (var i = 0; i < filesArray.length; i++) {
deferreds.push($.get(filesArray[i]));
}
$.when.apply($, deferreds).done(function() {
// all ajax calls are done and results are available now
// arguments[0][0] is the data from the first $.get call
// arguments[1][0] is the data from the second $.get call
// and so on
});
}
Working demo of this last option: http://jsfiddle.net/jfriend00/5ppU4/
FYI, there's no magic inside of $.when()
. If you look at the jQuery code for it, it is just keeping a counter of when the arguments passed to it are all done (similar to my first two options here). The main difference is that it's using the promise interface to the jqXHR object instead of knowledge that it's an ajax call. But conceptually, it's doing the same thing.
Here's one more using a new object I've written for handling multiple deferreds.
function loadFilesAndDoStuff(filesArray) {
var deferred = $.MultiDeferred().done(function() {
// all ajax calls are done and results are available now
// arguments[0][0] is the data from the first $.get call
// arguments[1][0] is the data from the second $.get call
// and so on
});
for (var i = 0; i < filesArray.length; i++) {
deferred.add($.get(filesArray[i]));
}
}
The MultiDeferred code is a jQuery plug-in specifically written to handle notifying you when multiple deferreds are done and the code for it is here:
jQuery.MultiDeferred = function(/* zero or more promises */) {
// make the Deferred
var self = jQuery.Deferred();
var remainingToFinish = 0;
var promises = [];
var args = [];
var anyFail = false;
var failImmediate = false;
function _add(p) {
// save our index in a local variable so it's available in this closure later
var index = promises.length;
// save this promise
promises.push(p);
// push placeholder in the args array
args.push([null]);
// one more waiting to finish
++remainingToFinish;
// see if all the promises are done
function checkDone(fail) {
return function() {
anyFail |= fail;
// make copy of arguments so we can save them
args[index] = Array.prototype.slice.call(arguments, 0);
--remainingToFinish;
// send notification that one has finished
self.notify.apply(self, args[index]);
// if all promises are done, then resolve or reject
if (self.state() === "pending" && (remainingToFinish === 0 || (fail && failImmediate))){
var method = anyFail ? "reject" : "resolve";
self[method].apply(self, args);
}
}
}
// add our own monitors so we can collect all the data
// and keep track of whether any fail
p.done(checkDone(false)).fail(checkDone(true));
}
// add a new promise
self.add = function(/* one or more promises or arrays of promises */) {
if (this.state() !== "pending") {
throw "Can't add to a deferred that is already resolved or rejected";
}
for (var i = 0; i < arguments.length; i++) {
if (arguments[i] instanceof Array) {
for (var j = 0; j < arguments[i].length; j++) {
_add(arguments[i][j]);
}
} else {
_add(arguments[i]);
}
}
return this;
}
// get count of remaining promises that haven't completed yet
self.getRemaining = function() {
return remainingToFinish;
}
// get boolean on whether any promises have failed yet
self.getFailYet = function() {
return anyFail;
}
self.setFailImmediate = function(failQuick) {
failImmediate = failQuick;
return this;
}
// now process all the arguments
for (var i = 0; i < arguments.length; i++) {
self.add(arguments[i]);
}
return self;
};
Create an array of each needed file, then loop through the array of files and call $.get
each iteration, and have it call a combining function that will combine the data and do a count check, once count is reached call callback.
function loadData(files,callback){
var combinedData = "";
var count = 0;
function combineFile(data){
count++;
combinedData += data;
if(count==files.length-1){
callback(combinedData);
}
}
for(var i=0; i<files.length; i++){
$.get(files[i],combineFile);
}
}
loadData(["files1.txt","files2.txt","files3.txt"],function(data){
console.log("Combined Data: ",data);
});