6

Possible Duplicate:
javascript: execute a bunch of asynchronous method with one callback

I've been struggling with this issue for days now, but I just can't figure out an elegant way to handle it. Here's the problem.

I'm running a forEach loop, and I need to know when everything has been completed. Since forEach loops are blocking, it should be easy enough to just drop a console.log after the forEach loop, which will run when the forEach loop is complete. However, if there is any function inside of the forEach loop that's not sync, this entire system breaks down very quickly, and I have no way of knowing when the contents of the loop are complete other than very strange hacky counters and if statements. My question is whether there is an elegant way to handle this situation in javascript.

Here are a couple examples:

In this case, the forEach loop works perfectly nicely, since everything in the loop is sync.

[1,2,3].forEach(function(num){
  console.log("we're on " + num);
});
console.log('done counting!')

//=> we're on 1
//=> we're on 2
//=> we're on 3
//=> done counting!

This is where we start running into problems though, as a basic example:

[1,2,3].forEach(function(num){
  setTimeout(function(){ console.log(num) }, 200);
});
console.log('done counting!');

//=> done counting!
//=> we're on 1
//=> we're on 2
//=> we're on 3

Although it does make sense that this is happening, I now have the unfortunate issue on my hands that I need a callback for when we are done counting, and there's no smooth way for me to have one other than something gross like this:

var counter = 0;
var numbers = [1,2,3]

var log_number = function(num){
  console.log("we're on " + num);
  counter++;
  if (counter == numbers.length) {
    console.log("done counting!")
  }
}

numbers.forEach(function(num){
  setTimeout(log_number(num), 200);
});

//=> we're on 1
//=> we're on 2
//=> we're on 3
//=> done counting!

Goodness, that's an awful lot of overhead. Is there any smoother way to make this happen? I've checked out promises, deferred's, and sequences, but I haven't really been able to implement any one of them in a way that makes this more concise : (

To be clear, it's not setTimeout specifically that is my issue, it generally having async functions running inside a sync function - that was just the simplest example.

Community
  • 1
  • 1
Jeff Escalante
  • 3,137
  • 1
  • 21
  • 30
  • You need a callback after end the each loop? – Gabriel Santos Nov 04 '12 at 00:43
  • Sorry to vote to close, but I think it's really the same as the solution in that question (share the same callback and keep a counter). THe only thing would be to fancy it up a bit perhaps with a registration system or something more complex than a counter. – Cade Roux Nov 04 '12 at 00:50

6 Answers6

7
Array.prototype.forEachDone = function(fn, scope, lastfn) {
    for(var i = 0, c = 0, len = this.length; i < len; i++) {
        fn.call(scope, this[i], i, this, function() {
            ++c === len && lastfn();
        });
    }
};

[1,2,3].forEachDone(function(num, i, arr, done){
  setTimeout(function(){ console.log(num); done() }, 200);
}, this, function() {
    console.log('done counting!');
});

this will do what you're looking for. forEachDone will take the exact same parameters as forEach, with an additional one at the end that is a callback to be called when the functions applied to each element of the array are done. the function that'll be applied to each element in the array also takes the exact same parameters as in forEach but also with an additional one that is a function that should be called when the function finishes.

PS: personally, i'd never touch a native object's prototype, but if you're looking for pretty, this is it.

zertosh
  • 5,333
  • 1
  • 17
  • 9
4
var list = ["your", "mom"];
var count = 0;

for(var i = 0; i < list.length; ++i) {
    getSomething(list[i], function() {
     if(++count == list.length)
      imDone();
     });
}

another way is like this

var biscuits = ["cheddar", "butter milk"];

(function eatBiscuits(location) {
  eatIt(biscuits[++location], whenAllEaten);
  typeof biscuits[location] != "undefined" && eatBiscuits(location);
}(-1))

function eatIt(biscuit, cb) {
 if (typeof biscuit == "undefined") {
  cb();
 } else {
  stuffMyFace();
 }
}
samccone
  • 10,746
  • 7
  • 43
  • 50
1

If you are willing to use a lib, then async will be a good choice (works in browsers as well as with node.js)!

async.parallel([
    function(){ ... },
    function(){ ... }
], callback);

async.series([
    function(){ ... },
    function(){ ... }
]);
lrsjng
  • 2,615
  • 1
  • 19
  • 23
  • Just stumbled across this library before seeing this answer, but looks really promising. I assume the same overhead is behind it, but at least it will look a little cleaner : ) – Jeff Escalante Nov 04 '12 at 01:38
1

I tend to use a little utility function like this:

​function asyncCallback( iterations, onComplete ){
  var total = iterations.length || iterations, count = 0;
  return function( func ){
    func();
    if ( ++count == total ) onComplete();
  };
}

var arr = [1,2,3];

var async = asyncCallback(arr, function(){
  console.log('donezo!');
});

arr.forEach(function(num){
  setTimeout(function(){
    async(function(){
      console.log(num);
    });
  }, 200);
});

Kevin Ennis
  • 14,226
  • 2
  • 43
  • 44
0

Actually there is no 'smart' way to do it. You need to use something like setTimeout, setInterval, Worker

Mustafa Genç
  • 2,569
  • 19
  • 34
-1
[1,2,3,4,5,6,7,8].forEach(function(n){
 setTimeout(function(){
  console.log(arguments[0]);
 },1000*n,n);
});

This works fine for me

Aditya APP
  • 59
  • 4
  • I'm not sure this is an any way relevant to the question... it doesn't solve my issue at all. It just counts slower, and doesn't offer any sort of callback when finished. – Jeff Escalante Nov 04 '12 at 00:41
  • The things you should care about when calling an async function from a sync loop is how to pass the arguments. `[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16].forEach(function(n){ (function(){ console.log(arguments[0]); })(n); });` – Aditya APP Nov 04 '12 at 00:46
  • Thats not what i care about. What I need is a callback for when the async functions inside of my sync loop are all finished. This is what I said in the question... – Jeff Escalante Nov 04 '12 at 01:01