JavaScript how to determine when AJAX calls are done in a loop

Viewed 70

I've spent the last few days trying to tackle this issue and have read all sorts of solutions on StackOverflow and other sites.

I'm building a site that grabs XML data from an outside source and then gets more XML depending on the results to build a network graph. The problem is that I have to essentially wait until this loop of AJAX calls (which may loop into more AJAX calls) is finished before drawing.

I don't know if this just has an especially high cognitive load, but it really has me stumped.

My Code:

function cont(outerNodes) {

    for (var i = 0; i < outerNodes.length; i++) {
        var node = outerNodes.pop();
        getXML(node["label"], node["id"]);
    } 
    // I want the code to wait until loop is done, and then draw.
    draw(nodes, edges);
}

function getXML(term, fromId) {

    var url = someURL;
    $.ajax({
        url: url,
        dataType: "xml",
        success: function(result) {
            var outerNodes = process(result, fromId, term);
            cont(outerNodes);
        }
    });

}

Note: I understand I may be completely misunderstanding JavaScript synchronicity here, and I very likely am. I have used callbacks and promises successfully in the past, I just can't seem to wrap my head around this one.

If I have not been totally clear, please let me know.

I did try implementing a counter of sorts that is incremented in the process() function, like so:

if (processCount < 15) {
    for (var i = 0; i < outerNodes.length; i++) {
        var node = outerNodes.pop();
        getXML(node["label"], node["id"]);
    }
} else {
    draw(nodes, edges);
}

However, this ended up with several draw() calls which made my performance abysmal.

4 Answers

There are nice new well-supported APIs and language constructs we can use. The Fetch API, await, and for...of loops.

The Fetch API uses Promises. Promises can be awaited. The for...of loop is aware of await and won't continue the loop until the await has passed.

// Loop through, one-at-a-time
for (const node of outerNodes) {
  // Make the HTTP request
  const res = await fetch(someUrl);
  
  // Do something with the response here...
}

Don't forget a try/catch (which also works with await), and check res.ok.

Brad's answer changes the code to by synchronious and to me that defeats the purpose. If you are constantly waiting on all request to be finished then it could take a while, while normal browsers can handle multiple requests.

The problem you have in your original questions is with scope. Since each call to cont(outerNodes) will trigger it's own scope, it has no idea what are calls are doing. So basically if you call cont(outerNodes) twice, each call will handle it's own list of outernodes and then call draw. The solution is to share information between the different scopes, which can be done with one, but preferably two global variables: 1 to track active processes and 1 to track errors.

var inProcess = 0;
var nrErrors = 0;

function cont(outerNodes) {

    //make sure you have outerNodes before you call outerNodes.length
    if (outerNodes) {
      for (var i = 0; i < outerNodes.length; i++) {
          var node = outerNodes.pop();
          inProcess++; //add one more in process
          getXML(node["label"], node["id"]);
      } 
    }

    //only trigger when nothing is in proces. 
    if (inProcess==0) {
      // I want the code to wait until loop is done, and then draw.
      draw(nodes, edges);
   }
}

function getXML(term, fromId) {

    var url = someURL;
    $.ajax({
        url: url,
        dataType: "xml",
        success: function(result) {
            var outerNodes = process(result, fromId, term);
            inProcess--; //one is done
            cont(outerNodes);
        },
        error: function() {
            inProcess--; //one is done
            nrErrors++; //one more error
            cont(null); //run without new outerNodes, to trigger a possible draw
        }
    });

}

Please note that I track nrErrors but dont do anything with it. You could use it to stop further processing all together or warn the user that the draw is not complete.

[important] Keep in mind that this works in javascript because at best it mimics multithreading. That means the the call to inProcess--; and then right after cont(outerNodes); is always execute directly after eachother.

If you would port this to a true multithreading environment, it could very well be that another scope/version of cont(null); would cut in between the two lines and there would still be multiple draws.

The best way to solve this question should be using either promise or callback. If you really want to avoid promise or callback(Although i don't know why...) You can try with a counter.

    let processCount = 0;
    
    // Increasing the processCount in getXML callback method
    function getXML(term, fromId) {
        var url = someURL;
        $.ajax({
            url: url,
            dataType: "xml",
            success: function(result) {
                processCount++;
                var outerNodes = process(result, fromId, term);
                cont(outerNodes);
            }
        });

    }

    for (var i = 0; i < outerNodes.length; i++) {
        var node = outerNodes.pop();
        getXML(node["label"], node["id"]);
    }
    while (processCount < outerNodes.length) {
        // do nothing, just wait'
    } 

    draw(nodes, edges);

If after testing it many times, you know that it will never take more than say 5 seconds... you can use a setTimeout.

function cont(outerNodes) {

        for (var i = 0; i < outerNodes.length; i++) {
            var node = outerNodes.pop();
            getXML(node["label"], node["id"]);
    } 


// Display a 5 second progress bar here

 setTimeout(function(){ draw(nodes, edges); },5000); 

}
Related