jQuery getting rid of nested ajax functions

2019-02-16 02:52发布

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 working with async functions, this ugly nesting comes up.

How can I avoid nesting functions when I really just want to wait for them each to complete? (I'm using jQuery if it helps)

 function loadFilesAndDoStuff() {
    $.get(firstFile, function(first_file_data) {
      $.get(secondFile, function(second_file_data) {
        $.get(thirdFile, function(third_file_data) {
          someOtherAsyncFunction(function(combined_file_data) {
            // do some stuff with the "combined_file_data".
          });
        });
      });
    });
  }

2条回答
老娘就宠你
2楼-- · 2019-02-16 02:58

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;    
};
查看更多
家丑人穷心不美
3楼-- · 2019-02-16 03:02

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);
});
查看更多
登录 后发表回答