所有异步forEach回调完成后的回调


244

如标题所示。我该怎么做呢?

我想whenAllDone()在forEach循环遍历每个元素并进行一些异步处理之后调用。

[1, 2, 3].forEach(
  function(item, index, array, done) {
     asyncFunction(item, function itemDone() {
       console.log(item + " done");
       done();
     });
  }, function allDone() {
     console.log("All done");
     whenAllDone();
  }
);

有可能让它像这样工作吗?当forEach的第二个参数是一个回调函数,该函数一旦经过所有迭代便会运行?

预期产量:

3 done
1 done
2 done
All done!

13
如果标准数组forEach方法具有done回调参数和allDone回调,那就太好了!
Vanuan

22
如此简单的事情需要在JavaScript中进行大量工作,真是太可惜了。
阿里

Answers:


409

Array.forEach 不能提供这种功能(哦,如果可以的话),但是有几种方法可以实现您想要的功能:

使用一个简单的计数器

function callback () { console.log('all done'); }

var itemsProcessed = 0;

[1, 2, 3].forEach((item, index, array) => {
  asyncFunction(item, () => {
    itemsProcessed++;
    if(itemsProcessed === array.length) {
      callback();
    }
  });
});

(由于@vanuan等),这种方法可确保在调用“完成”回调之前处理所有项目。您需要使用在回调中更新的计数器。依赖于index参数的值不能提供相同的保证,因为不能保证异步操作的返回顺序。

使用ES6承诺

(承诺库可用于较旧的浏览器):

  1. 处理所有保证同步执行的请求(例如1到2然后3)

    function asyncFunction (item, cb) {
      setTimeout(() => {
        console.log('done with', item);
        cb();
      }, 100);
    }
    
    let requests = [1, 2, 3].reduce((promiseChain, item) => {
        return promiseChain.then(() => new Promise((resolve) => {
          asyncFunction(item, resolve);
        }));
    }, Promise.resolve());
    
    requests.then(() => console.log('done'))
    
  2. 处理所有异步请求而无需“同步”执行(2个完成可能比1个完成快)

    let requests = [1,2,3].map((item) => {
        return new Promise((resolve) => {
          asyncFunction(item, resolve);
        });
    })
    
    Promise.all(requests).then(() => console.log('done'));
    

使用异步库

还有其他异步库(异步是最流行的),它们提供了表达所需内容的机制。

编辑

对问题的正文进行了编辑,以删除以前同步的示例代码,因此我更新了答案以澄清问题。原始示例使用类似同步的代码来建模异步行为,因此适用以下内容:

array.forEach同步的,也是如此res.write,因此您只需在调用foreach之后放置回调即可:

  posts.foreach(function(v, i) {
    res.write(v + ". index " + i);
  });

  res.end();

31
但是请注意,如果在forEach中存在异步内容(例如,您正在遍历URL数组并对其进行HTTP GET),则不能保证res.end将在最后被调用。
AlexMA 2013年

为了在循环中执行异步操作后触发回调,您可以使用异步实用程序的each方法:github.com/caolan/async#each
elkelk 2014年

2
@Vanuan我已经更新了我的答案,以更好地匹配您相当重要的编辑:)
Nick Tomlin

4
为什么不只是if(index === array.length - 1)删除itemsProcessed
Amin Jafari

5
@AminJafari,因为异步调用可能无法按照注册时的确切顺序解析(例如,您正在呼出到服务器,并且在第二次调用时会稍稍停顿,但可以处理最后一次调用)。最后一个异步调用可以在之前的调用之前解析。更改计数器可以防止这种情况发生,因为所有回调都必须触发,无论它们解析的顺序如何。
尼克·汤姆林

25

如果您遇到异步函数,并且想要确保在执行代码之前完成其任务,那么我们始终可以使用回调功能。

例如:

var ctr = 0;
posts.forEach(function(element, index, array){
    asynchronous(function(data){
         ctr++; 
         if (ctr === array.length) {
             functionAfterForEach();
         }
    })
});

注意:functionAfterForEach是foreach任务完成后要执行的功能。 asynchronous是在foreach内部执行的异步函数。


9
由于不注意异步请求的执行顺序,因此无法使用。最后一个异步请求可以在其他请求之前结束,并在所有请求完成之前执行functionAfterForEach()。
雷米·戴维

@RémyDAVID是的,您对执行的顺序有一点看法,或者我要说说该过程完成了多长时间,javascript是单线程的,所以最终可以工作。证明就是对这个答案的认可。
EmilReñaEnriquez 2015年

1
我不太确定您为什么有这么多赞成票,但是Rémi是正确的。您的代码根本无法工作,因为异步意味着任何请求都可能随时返回。尽管JavaScript不是多线程的,但您的浏览器却是。我可能会加重。因此,它可以随时以任何顺序调用您的任何回调,具体取决于何时从服务器收到回复……
Alexis Wilke

2
是的,这是完全错误的答案。如果我并行运行10次下载,则几乎可以保证最后一次下载比其余下载提前完成,从而终止执行。
knrdk '16

我建议您使用一个计数器来增加已完成的异步任务的数量,并将其与数组而不是索引的长度相匹配。投票的数量与答案的正确性无关。
亚历克斯

17

希望这能解决您的问题,当我需要在内部执行异步任务的情况下执行forEach时,我通常会使用此方法。

foo = [a,b,c,d];
waiting = foo.length;
foo.forEach(function(entry){
      doAsynchronousFunction(entry,finish) //call finish after each entry
}
function finish(){
      waiting--;
      if (waiting==0) {
          //do your Job intended to be done after forEach is completed
      } 
}

function doAsynchronousFunction(entry,callback){
       //asynchronousjob with entry
       callback();
}

我在Angular 9代码中遇到了类似的问题,这个答案为我解决了问题。尽管@EmilReñaEnriquez的答案对我也有用,但我发现这是针对此问题的更准确,更简单的答案。
omostan

17

异步情况下给出了多少不正确的答案,这很奇怪!可以简单地表明检查索引不提供预期的行为:

// INCORRECT
var list = [4000, 2000];
list.forEach(function(l, index) {
    console.log(l + ' started ...');
    setTimeout(function() {
        console.log(index + ': ' + l);
    }, l);
});

输出:

4000 started
2000 started
1: 2000
0: 4000

如果我们检查index === array.length - 1,则在第一个迭代完成时将调用回调,而第一个元素仍处于等待状态!

为了不使用异步之类的外部库来解决此问题,我认为最好的办法是在每次迭代后保存列表的长度和减量。因为只有一个线程,所以我们确定没有竞争条件的机会。

var list = [4000, 2000];
var counter = list.length;
list.forEach(function(l, index) {
    console.log(l + ' started ...');
    setTimeout(function() {
        console.log(index + ': ' + l);
        counter -= 1;
        if ( counter === 0)
            // call your callback here
    }, l);
});

1
那可能是唯一的解决方案。异步库也使用计数器吗?
Vanuan 2015年

1
尽管其他解决方案也能胜任,但这是最引人注目的,因为它不需要链接或增加复杂性。吻
azatar '17

请同时考虑数组长度为零的情况,在这种情况下,将永远不会调用回调
Saeed Ir

6

使用ES2018,您可以使用异步迭代器:

const asyncFunction = a => fetch(a);
const itemDone = a => console.log(a);

async function example() {
  const arrayOfFetchPromises = [1, 2, 3].map(asyncFunction);

  for await (const item of arrayOfFetchPromises) {
    itemDone(item);
  }

  console.log('All done');
}

1
Node v10中的Availabe
Matt Swezey

2

我没有Promise的解决方案(这可以确保每个操作在下一个操作开始之前就已结束):

Array.prototype.forEachAsync = function (callback, end) {
        var self = this;
    
        function task(index) {
            var x = self[index];
            if (index >= self.length) {
                end()
            }
            else {
                callback(self[index], index, self, function () {
                    task(index + 1);
                });
            }
        }
    
        task(0);
    };
    
    
    var i = 0;
    var myArray = Array.apply(null, Array(10)).map(function(item) { return i++; });
    console.log(JSON.stringify(myArray));
    myArray.forEachAsync(function(item, index, arr, next){
      setTimeout(function(){
        $(".toto").append("<div>item index " + item + " done</div>");
        console.log("action " + item + " done");
        next();
      }, 300);
    }, function(){
        $(".toto").append("<div>ALL ACTIONS ARE DONE</div>");
        console.log("ALL ACTIONS ARE DONE");
    });
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="toto">

</div>


1
 var counter = 0;
 var listArray = [0, 1, 2, 3, 4];
 function callBack() {
     if (listArray.length === counter) {
         console.log('All Done')
     }
 };
 listArray.forEach(function(element){
     console.log(element);
     counter = counter + 1;
     callBack();
 });

1
这将不起作用,因为如果在foreach中进行了异步操作。
Sudhanshu Gaur


0

我的解决方案:

//Object forEachDone

Object.defineProperty(Array.prototype, "forEachDone", {
    enumerable: false,
    value: function(task, cb){
        var counter = 0;
        this.forEach(function(item, index, array){
            task(item, index, array);
            if(array.length === ++counter){
                if(cb) cb();
            }
        });
    }
});


//Array forEachDone

Object.defineProperty(Object.prototype, "forEachDone", {
    enumerable: false,
    value: function(task, cb){
        var obj = this;
        var counter = 0;
        Object.keys(obj).forEach(function(key, index, array){
            task(obj[key], key, obj);
            if(array.length === ++counter){
                if(cb) cb();
            }
        });
    }
});

例:

var arr = ['a', 'b', 'c'];

arr.forEachDone(function(item){
    console.log(item);
}, function(){
   console.log('done');
});

// out: a b c done

解决方案是创新的,但是会出现错误-“任务不是功能”
Genius

0

我尝试使用Easy Way来解决它,并与您分享:

let counter = 0;
            arr.forEach(async (item, index) => {
                await request.query(item, (err, recordset) => {
                    if (err) console.log(err);

                    //do Somthings

                    counter++;
                    if(counter == tableCmd.length){
                        sql.close();
                        callback();
                    }
                });

request是Node js中mssql库的功能。这可以替换您想要的每个功能或代码。祝好运


0
var i=0;
const waitFor = (ms) => 
{ 
  new Promise((r) => 
  {
   setTimeout(function () {
   console.log('timeout completed: ',ms,' : ',i); 
     i++;
     if(i==data.length){
      console.log('Done')  
    }
  }, ms); 
 })
}
var data=[1000, 200, 500];
data.forEach((num) => {
  waitFor(num)
})

-2

您不需要回调即可遍历列表。只需end()在循环后添加呼叫即可。

posts.forEach(function(v, i){
   res.write(v + ". Index " + i);
});
res.end();

3
不。OP强调异步逻辑将在每次迭代中执行。res.write不是异步操作,因此您的代码将无法工作。
Jim G.

-2

一个简单的解决方案如下

function callback(){console.log("i am done");}

["a", "b", "c"].forEach(function(item, index, array){
    //code here
    if(i == array.length -1)
    callback()
}

3
不适用于异步代码,这是问题的全部前提。
grg

-3

setInterval如何检查完整的迭代计数会带来保证。不知道它是否不会使范围超载,但我使用它,似乎是一个

_.forEach(actual_JSON, function (key, value) {

     // run any action and push with each iteration 

     array.push(response.id)

});


setInterval(function(){

    if(array.length > 300) {

        callback()

    }

}, 100);

这看起来在逻辑上很简单
Zeal Murapa
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.