skip to Main Content

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


  1. 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); 
    
    }
    
    Login or Signup to reply.
  2. 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);
    
    Login or Signup to reply.
  3. 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.

    Login or Signup to reply.
  4. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search