views:

128

answers:

7

I want to get two resources using two asynch calls. I want to proceed only when both resources have been retrieved.

How can I do this elegantly in JS?

This would work:

getStuff1(function (result1) {
    getStuff2 (function (result2) {
        // do stuff with result1 and result2
        ....
    }
}

but stuff2 only starts after stuff1 completes. I'd prefer to start stuff2 while waiting on stuff1.

+3  A: 

One way is to use the same callback for both requests and proceed when both are complete:

var requestStatus = {
  fooComplete: false,
  barComplete: false
};

function callback(data) {
  if (isFoo(data)) {
    requestStatus.fooComplete = true;
  } else if (isBar(data)) {
    requestStatus.barComplete = true;
  }

  if (requestStatus.fooComplete && requestStatus.barComplete) {
    proceed();
  }
}

getAsync("foo", callback);
getAsync("bar", callback);

You'll probably want to flesh this out into a class.

Edit: added the async calls for clarity

lawnsea
a good idea, but a bit too much typing involved, and the extra object makes it less powerful (harder to maintain). Also, please note that there are **no classes in javascript**. :)
galambalazs
That's why I said it was *one* way to do it. :) Your answers above are two other good ways. Tracking the status of the tasks can be useful in situations where outside events might affect the state of things. Regarding the question of JS classes, I'm aware that the language doesn't provide such a beast. Unfortunately, both 'prototype' and 'Prototype' are bound in our scope. I haven't figured out a better word to describe an encapsulated bit of state and functionality that can be instantiated multiple times. Maybe we should call them 'Thingamajiggies'? :)
lawnsea
+3  A: 

You could have the callback function for each one indicate that their respective request has come back, and then execute the same common function. To illustrate:

var call1isBack = false;
var call2isBack = false;

function call1Callback() {
  call1isBack = true;
  commonCallback();
}

function call2Callback() {
  call2isBack = true;
  commonCallback();
}

function commonCallback() {
  if (call1isBack && call2isBack) {
    // do stuff, since you know both calls have come back.
  }
}
pkaeding
almost how I would do. See my answer.. :)
galambalazs
+1  A: 

Use a common callback handler with a counter that only allows passage into the "actual" processing section after the counter meets or exceeds the number of pending requests:

var commonHandler = (function() {
  var counter=0, pendingCalls=2;
  return function() {
    if (++counter >= pendingCalls) {
      // Do the actual thing...
    }
  }
})();

makeAjaxCall({args:args1, onComplete:commonHandler});
makeAjaxCall({args:args2, onComplete:commonHandler});

Using a closure around the anonymous function lets you avoid using a global variable for the counter.

maerics
+8  A: 

If you know that functions are in fact first-class objects in Javascript, you can come up with a fairly elegant solution.

Without any extra objects, or global variables.

function callback1() {
  callback1.done = true;
  commonCallback();
}

function callback2() {
  callback2.done = true;
  commonCallback();
}

function commonCallback() {
  if (callback1.done && callback2.done) {
    // do stuff, since you know both calls have come back.
  }
}

Why is this so elegant? Because you've encapsulated the data, your scope is free from useless variables and the code is more readable than ever. How cool is that? :)


UPDATE

And if you want a bit more general solution you may try the following:

function callback() {
  callback.number -= 1;
  if (callback.number === 0) {
    // do stuff since all calls finished
    callback.last();
  }
}
callback.newQueue = function(num, last) {
  callback.number = num;
  callback.last   = last;
}

// EXAMPLE USAGE

// our last function to be invoked
function afterEverythingDone(){ alert("done"); }

// create a new callback queue
callback.newQueue(3, afterEverythingDone);

// as time passes you call the callback
// function after every request
callback();
callback();
callback();

// after all call is finished
// afterEverythingDone() executes

Awesomeness again :)

galambalazs
But aren't both callback1 and callback2 useless variables since there only purpose is to mark the completion of the Ajax calls? Couldn't that functionality be entirely encapsulated in the common callback?
maerics
they are not useless. They could be merged of course, but in this form it will be very intuitive that after the first call finishes he calls callback1, and when the second call finishes he calls callback2. And if everything went well without any further typing the real callback will be called.
galambalazs
I may append my answer with a more general alternative, but the problem he mention in his question will be very easy to handle with this form.
galambalazs
I *really* like this solution. It's very similar to some production code I wrote a few months ago. Like most things in software engineering, however, YMMV. There are a couple of corner cases worth pointing out. **(1)** The first snippet assumes that commonCallback has the callback variables in scope. This can be a problem sometimes. Anonymous functions are an example. **(2)** The second solution doesn't actually meet the spec if callbacks can get fired multiple times. In that case, `callback1(); callback2();` is dealt with the same way as `callback1(); callback1();`. Still awesome, though.
lawnsea
Thanks for the comment. I think the second solution does meet the spec, because the OP said *"get two resources using two asynch calls"*. I assume that a resource can't be ready twice, or three times, only once. Of course the callback could have got an array, and we could save the results of what the callbacks return so that the final commonCallback would know which resource may have failed to load.
galambalazs
A: 

Here's a snippet from a concurrent library I'm working on. All you need to do is instantiate a new Concurrent.Counter with the number of requests to await (before you execute them), and the callback to execute when they have finished. Before each of the asynchronous functions returns, have it call the decrement() method of the counter; once the counter has been decremented the number of times specified, the callback will be executed:

// Ensure the existence of the "Concurrent" namespace
var Concurrent = Concurrent || {};

/**
 * Constructs a new Concurrent.Counter which executes a callback once a given number of threads have
 * returned. Each Concurrent.Counter instance is designed to be used only once, and then disposed of,
 * so a new one should be instantiated each additional time one is needed.
 *
 * @param {function} callback The callback to execute once all the threads have returned
 * @param {number} count The number of threads to await termination before executing the callback
 */
Concurrent.Counter = function(callback, count) {

    /**
     * Decrements the thread count, and executes the callback once the count reaches zero.
     */
    this.decrement = function() {
        if (!(-- count)) {
            callback();
        }
    };
};

// The number of asynchronous requests to execute
var requests = 10,

// Executes a callback once all the request tasks have returned
counter = new Concurrent.Counter(function() {
    // this will be executed once the tasks have completed
}, requests),

// Tracks the number of requests made
i;

for (i = 0; i < requests; i ++) {
    setTimeout(function() {
        /*
         * Perform an asynchronous task
         */

        // Decrement the counter
        counter.decrement();
    }, 0);
}
Dylon Edwards
A: 

I use PingPong class to do this. For example:

var pp = new PingPong();
pp.ping(2, function() {
    // do stuff after works A and B are done
});
$.getJSON('./A.json', function(A) {
    // do work A
    pp.pong();
});
$.getJSON('./B.json', function(B) {
    // do work B
    pp.pong();
});
Vytautas Jakutis
A: 

This is written off the top of my head, but it should work.

function createCoordinator(onFinish) {
    var count = 0;
    return function (callback) {
        count++;
        return function () {
            if (callback.apply(this, arguments))
                count--;
            if (count == 0)
                onFinish();
        }
    }
}

var coordinate = createCoordinator(function () { alert('done!') });

// Assume sendAJAX = function (url, onreadystatechange)
sendAJAX('url1', coordinate(function () {
    if (this.readyState != 4) 
        return false; // Return false if not done
    alert('Done with url1!');
    return true;
}));
sendAJAX('url2', coordinate(function () {
    if (this.readyState != 4) 
        return false; // Return false if not done
    alert('Done with url2!');
    return true;
}));
Casey Hope