async.js each get index in iterator
Asked Answered
S

6

36

I'm using caolan's async.js library, specifically the .each method.

How do you get access to the index in the iterator?

async.each(ary, function(element, callback){
  //do stuff here for each element in ary
  //how do I get access to the index?
}, function(err) {
  //final callback here
})
Skimpy answered 7/7, 2013 at 22:21 Comment(2)
You could try github.com/kevireilly/eachtick which does what async.each should've done. Seriously, who forgets to add an index to an interation script?Pignus
why not use async.eachSeries, and then add you own index outside of the function? async.eachSeries should guarantee that the functions are not run concurrently, so using an external index variable should be fine.Hymenium
H
40

You can use async.forEachOf - it calls its iterator callback with the index as its second argument.

> async.forEachOf(['a', 'b', 'c'], function () {console.log(arguments)});
{ '0': 'a', '1': 0, '2': [Function] }
{ '0': 'b', '1': 1, '2': [Function] }
{ '0': 'c', '1': 2, '2': [Function] }

see the docs for more info.

Healall answered 5/8, 2015 at 1:17 Comment(2)
This should be the accepted answer. The current answer is obsolete.Unmannered
@Unmannered You're right, it should be. I'll edit my answer to point at itErdah
E
37

Update

Since writing this answer, there is now a better solution. Please see xuanji's answer for details

Original

Thanks to @genexp for a simple and concise example in the comments below...

async.each(someArray, function(item, done){
    console.log(someArray.indexOf(item));
});

Having read the docs, I suspected that there wasn't any way to access an integer representing position in the list...

Applies an iterator function to each item in an array, in parallel. The iterator is called with an item from the list and a callback for when it has finished. If the iterator passes an error to this callback, the main callback for the each function is immediately called with the error.

Note, that since this function applies the iterator to each item in parallel there is no guarantee that the iterator functions will complete in order.

So I dug a little deeper (Source code link)

async.each = function (arr, iterator, callback) {
        callback = callback || function () {};
        if (!arr.length) {
            return callback();
        }
        var completed = 0;
        _each(arr, function (x) {
            iterator(x, only_once(function (err) {
                if (err) {
                    callback(err);
                    callback = function () {};
                }
                else {
                    completed += 1;
                    if (completed >= arr.length) {
                        callback(null);
                    }
                }
            }));
        });
    };

As you can see there's a completed count which is updated as each callback completes but no actual index position.

Incidentally, there's no issue with race conditions on updating the completed counter as JavaScript is purely single-threaded under the covers.

Edit: After digging further into the iterator, it looks like you might be able to reference an index variable thanks to closures...

async.iterator = function (tasks) {
    var makeCallback = function (index) {
        var fn = function () {
            if (tasks.length) {
                tasks[index].apply(null, arguments);
            }
            return fn.next();
        };
        fn.next = function () {
            return (index < tasks.length - 1) ? makeCallback(index + 1): null;
        };
        return fn;
    };
    return makeCallback(0);
};
Erdah answered 7/7, 2013 at 22:40 Comment(3)
I think to summarize here, the answer is "NO" you can't, which is annoying, but also by design due to the way each parallelizes by default. However, due to the magic of closure you DO have access to your original array, so something like this should work: async.each(someArray, function(item, done){console.log(someArray.indexOf(item));});Calmas
@basic, can you have a look at an async related question here (#27646535)?Bloated
@IstiaqueAhmed I will, of course, have a look but I can't promise I can help.Erdah
S
9

Just use async.forEachOf().

async.forEachOf(arr, function(value, index, callback) { ... }, ...

For detail, see documentation here.

Scotch answered 18/3, 2016 at 6:30 Comment(0)
G
3

The method async.each() will iterate the array in parallel, and it doesn't provide the element's index to the iterating callback.

So when you have:

function( done ){

  async.each(

    someArray,

    function( item, cb ){
      // ... processing

      cb( null );
    },

    function( err ){
      if( err ) return done( err );

      // ...
      done( null );
    }
  );
}

While you COULD use Array.indexOf() to find it:

function( done ){

  async.each(

    someArray,

    function( item, cb ){
      // ... processing

      var index = someArray.indexOf( item );

      cb( null );
    },

    function( err ){
      if( err ) return done( err );

      // ...
      done( null );
    }
  );
}

This requires an in-memory search in the array for EVERY iteration of the array. For large-ish arrays, this might slow everything down quite badly.

A better workaround could be by using async.eachSeries() instead, and keep track of the index yourself:

function( done ){

  var index = -1;
  async.eachSeries(

    someArray,

    function( item, cb ){

      // index is updated. Its first value will be `0` as expected
      index++;

      // ... processing

      cb( null );
    },

    function( err ){
      if( err ) return done( err );

      // ...
      done( null );
    }
  );
}

With eachSeries(), you are guaranteed that things will be done in the right order.

Another workaround, which is the async's maintainer's first choice, is to iterate with Object.keys:

function( done ){

  async.each(

    Object.keys( someArray ),

    function( key, cb ){

      // Get the value from the key          
      var item = someArray[ key ];

      // ... processing

      cb( null );
    },

    function( err ){
      if( err ) return done( err );

      // ...
      done( null );
    }
  );
}

I hope this helps.

Girhiny answered 8/3, 2015 at 23:23 Comment(0)
M
1

modern-async's forEach() implementation is similar but gives you access to the index.

Mortician answered 20/12, 2020 at 19:35 Comment(0)
F
0
  1. indexOf solution is slow and should not be used for large arrays.
  2. eachSeries solution by Merc does't make what you want.
  3. Effective workaround is just to build another array with indexes:
someArrayWithIndexes = someArray.map(function(e, i) {return {obj: e, index: i}});
async.each(someArrayWithIndexes , function(item, done){
    console.log("Index:", item.index);
    console.log("Object:", item.obj);
});
Felipafelipe answered 20/5, 2015 at 20:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.