3

I need to visit each node in a tree, do some asynchronous work, and then find out when all of the asynchronous work has completed. Here are the steps.

  1. Visit a node and modify its children asynchronously.
  2. When async modifications to children are done, visit all children (which might require async work).
  3. When all asynchronous work for all descendants is done, do something else.

Update:

I ended up using a pattern that looks like a monitor/lock (but isn't) for each node to know when to begin step 2. I used events and attributes to keep track of all descendants of a node to know when to begin step 3.

It works, but man is this difficult to read! Is there a cleaner pattern?

function step1(el) { // recursive
  var allDone = false;
  var monitor = new Monitor();
  var lock = monitor.lock(); // obtain a lock
  $(el).attr("step1", ""); // step1 in progress for this node

  // fires each time a descendant node finishes step 1
  $(el).on("step1done", function (event) {
    if (allDone) return;
    var step1Descendants = $(el).find("[step1]");
    if (step1Descendants.length === 0) {
      // step 1 done for all descendants (so step 2 is complete)
      step3(el); // not async
      allDone = true;
    }
  });

  // fires first time all locks are unlocked
  monitor.addEventListener("done", function () {
    $(el).removeAttr("step1"); // done with step 1
    step2(el); // might have async work
    $(el).trigger("step1done");
  });

  doAsyncWork(el, monitor); // pass monitor to lock/unlock
  lock.unlock(); // immediately checks if no other locks outstanding
};

function step2(el) { // visit children
  $(el).children().each(function (i, child) {
    step1(child);
  });
};
Chris Calo
  • 7,518
  • 7
  • 48
  • 64
  • 4
    Keep a work-in-progress counter? Add a job, increment counter; finish a job, decrease counter. When counter hits zero, you're done. – Marc B Jan 25 '12 at 20:23
  • It's looking like promises is the pattern I'm looking for. Working on a solution now… – Chris Calo Jan 31 '12 at 06:00

3 Answers3

3

Here's an updated version that walks the node-tree, processing each child in the initial root node, and then descends recursively into each child's tree and processes its child nodes and so on.

Here's a jsfiddle demo

// Pass the root node, and the callback to invoke
// when the entire tree has been processed
function processTree(rootNode, callback) {
    var i, l, pending;

    // If there are no child nodes, just invoke the callback
    // and return immediately
    if( (pending = rootNode.childNodes.length) === 0 ) {
        callback();
        return;
    }

    // Create a function to call, when something completes
    function done() {
        --pending || callback();
    }

    // For each child node
    for( i = 0, l = rootNode.childNodes.length ; i < l ; i++ ) {
        // Wrap the following to avoid the good ol'
        // index-closure-loop issue. Pass the function
        // a child node
        (function (node) {

            // Process the child node asynchronously.
            // I'm assuming the function takes a callback argument
            // it'll invoke when it's done.
            processChildNodeAsync(node, function () {

                // When the processing is done, descend into
                // the child's tree (recurse)
                processTree(node, done);

            });

        }(rootNode.childNodes[i]));
    }
}

Original Answer

Here's a basic example you might be able to use... though without the specifics of your problem, it's half psuedo-code

function doAsyncTreeStuff(rootNode, callback) {
    var pending = 0;

    // Callback to handle completed DOM node processes
    // When pending is zero, the callback will be invoked
    function done() {
        --pending || callback();
    }

    // Recurse down through the tree, processing each node
    function doAsyncThingsToNode(node) {
        pending++;

        // I'm assuming the async function takes some sort of
        // callback it'll invoke when it's finished.
        // Here, we pass it the `done` function
        asyncFunction(node, done);

        // Recursively process child nodes
        for( var i = 0 ; i < node.children.length ; i++ ) {
            doAsyncThingsToNode(node.children[i]);
        }
    }

    // Start the process
    doAsyncThingsToNode(rootNode);
}
Flambino
  • 18,507
  • 2
  • 39
  • 58
  • This definitely helps somewhat, but it misses one important point that I didn't explain well. I'll update the question. – Chris Calo Jan 26 '12 at 06:27
  • 1
    @ChristopherJamesCalo Assuming `asyncFunction` in my example is the one that first modifies the child nodes, it's a simple matter of moving the for-loop into `asyncFunction`'s callback. `asyncFunction` itself would work pretty much the same as example (increment/decrement a counter, and invoke a callback when finished). That way, `asyncFunction` modifies the child nodes (step 1), and once it's done, the loop recurses to the child nodes (step 2). – Flambino Jan 26 '12 at 11:48
  • 1
    @ChristopherJamesCalo Actually, ignore the above - it's not accurate. Point is, that the basic setup in my example (the `pending` counter and `done` function) will work for any work of this sort. You're doing a 2-step async process, so you'd to use code like this in those 2 places (or break it out into a class). One to keep track of child node modification, and one to keep track of subsequent child node processing. Or, each child node could be modified, and then visited (step 1 + 2) before calling back. – Flambino Jan 26 '12 at 11:58
  • Flambino, might you modify your answer to reflect your latest comment? I'm having trouble making the leap from the code in your answer to the multi-step version. In particular, I'm wondering if there's a cleaner way of knowing when all async work on descendants is done without having to use attributes and events. – Chris Calo Jan 28 '12 at 06:00
  • 1
    @ChristopherJamesCalo I see you've found a solution yourself, but I'd be happy to update my answer later (haven't got the time right this moment) – Flambino Jan 29 '12 at 16:08
  • Thanks for your help, Flambino. I ended up liking the Promises pattern more than a pending counter. I posted my solution as an answer. – Chris Calo Feb 01 '12 at 04:50
  • @ChristopherJamesCalo Neat. Big fan of that pattern myself. Didn't know you were using jQuery or I'd have suggested it here too. But for a problem like yours, I probably still would've ended up with the solution you see, unless I wanted greater decoupling or needed to inform multiple observers of the process' completion. – Flambino Feb 01 '12 at 07:58
2

It seems the right pattern for this problem and for async work in general is Promises. The idea is that any function that will do asynchronous work should return a promise object, to which the caller can attach functions that should be called when the asynchronous work is completed.

jQuery has a great API for implementing this pattern. It's called a jQuery.Deferred object. Here's a simple example:

function asyncWork() {
  var deferred = $.Deferred();
  setTimeout(function () {
    // pass arguments via the resolve method
    deferred.resolve("Done.");
  }, 1000);
  return deferred.promise();
}

asyncWork().then(function (result) {
  console.log(result);
});

Very tidy. What's the difference between a Deferred object and its promise object? Good question.

Here's how you might apply this pattern to solve this problem.

function step1(el) { // recursive
  var deferred = $.Deferred();

  // doAsyncWork needs to return a promise
  doAsyncWork(el).then(function () {
    step2(el).then(function () {
      step3(el); // not async
      deferred.resolve();
    });
  });
  return deferred.promise();
};

function step2(el) { // visit children
  var deferred = $.Deferred();
  var childPromises = [];
  $(el).children().each(function (i, child) {
    childPromises.push(step1(child));
  });

  // When all child promises are resolved…
  $.when.apply(this, childPromises).then(function () {
    deferred.resolve();
  });
  return deferred.promise();
};

So much cleaner. So much easier to read.

Community
  • 1
  • 1
Chris Calo
  • 7,518
  • 7
  • 48
  • 64
  • You should try Async Tree Pattern. It's a declarative way to write asynchronous code without callbacks, promises and async / await abstractions – Guseyn Ismayylov Mar 26 '19 at 11:04
0

This is something you would probably prefer to do with threads to continue other work, but since you are using JavaScript you need to work around this with some sort of blocking. One way is make an initially empty list of finished tasks, make the asynchronous calls, and have each call register itself on the list when it is finished. While you are waiting for the calls, enter a loop with a timer, and at each iteration check if the finished tasks list is complete; if so, continue with other tasks. You may want to give up if your loop runs too long.

Glenn
  • 6,455
  • 4
  • 33
  • 42