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

297

正如标题所示,我该如何做到这一点?

我想在forEach循环已经遍历了每个元素并完成了一些异步处理之后调用whenAllDone()

[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!

18
如果标准数组的 forEach 方法有一个 done 回调参数和一个 allDone 回调,那就太好了! - Vanuan
37
在JavaScript中,一些看似简单的事情却需要经过许多琐碎的操作,这真是令人遗憾。 - Ali
18个回答

516

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和其他人的帮助,这种方法确保在调用“done”回调之前处理所有项目。您需要使用在回调中更新的计数器。根据索引参数的值不提供相同的保证,因为异步操作的返回顺序不能保证。

使用ES6 Promises

(可以为旧浏览器使用Promise库):

  1. Process all requests guaranteeing synchronous execution (e.g. 1 then 2 then 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. Process all async requests without "synchronous" execution (2 may finish faster than 1)

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

使用异步库

还有其他异步库,async 是最受欢迎的,它提供了表达你想要的内容的机制。


问题的主体已经被编辑以删除之前同步示例代码,因此我更新了我的答案以澄清。

原始示例使用同步代码来模拟异步行为,因此以下内容适用:

array.forEach同步的,res.write也是如此,因此您可以在调用foreach后直接放置回调函数:

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

  res.end();

36
注意,如果forEach内部有异步操作(例如,您正在遍历一个URL数组并对其进行HTTP GET),那么不能保证res.end将最后被调用。 - AlexMA
7
为什么不直接使用 if (index === array.length - 1),并删除 itemsProcessed 呢? - Amin Jafari
6
由于异步调用可能无法按照注册的顺序解决(例如,您在调用服务器时遇到了停顿,第二个调用被延迟了一会儿,但最后一个调用正常处理)。最后一个异步调用可能会在前面的调用之前解决。通过改变计数器来防范这种情况,因为所有回调都必须触发,而不管它们的解决顺序如何。 - Nick Tomlin
为什么不使用 if(index === array.length) { 而不是 if(itemsProcessed === array.length) {?这样可以节省一个变量的内存和增量处理。 - Inzamam Malik
1
请考虑数组长度为零的情况,此时回调函数将永远不会被调用。 - Saeed Ir
显示剩余5条评论

34
如果遇到异步函数,并且想要确保在执行代码之前完成任务,我们可以始终使用回调功能。
例如:
var ctr = 0;
posts.forEach(function(element, index, array){
    asynchronous(function(data){
         ctr++; 
         if (ctr === array.length) {
             functionAfterForEach();
         }
    })
});
注意:functionAfterForEach是在foreach任务完成后要执行的函数。asynchronous是在foreach内部执行的异步函数。

4
大家好,由于ES6的最新更新包含了Promises和Async/await功能,因此更好地利用这些功能。目前这种解决方案已经过时。 - Emil Reña Enriquez
这在一个空数组中不起作用。 - undefined

18

希望这可以解决你的问题,我通常在需要执行异步任务的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
      } 
}

需要翻译的内容为 "with"。
function doAsynchronousFunction(entry,callback){
       //asynchronousjob with entry
       callback();
}

我在我的Angular 9代码中遇到了类似的问题,这个答案对我很有帮助。虽然@Emil Reña Enriquez的答案也适用于我,但我发现这个答案更准确、更简单。 - omostan

18

很奇怪,有这么多错误答案被给出来了关于 异步 的情况!简单地说,检查索引并不能提供预期的行为:

// 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,回调函数将在第一次迭代完成时被调用,而第一个元素仍然是未决的!

为了解决这个问题,不使用外部库(如async),我认为你最好的选择是保存列表长度并在每次迭代后递减。由于只有一个线程,我们可以确保没有竞争条件。

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
1
尽管其他解决方案也能完成任务,但这个方案最具有说服力,因为它不需要链接或增加复杂性。K.I.S.S. - 4Z4T4R
请考虑数组长度为零的情况,此时回调函数将永远不会被调用。 - Saeed Ir

11

使用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');
}

2
仅支持 Node v10 - Matt Swezey
即使最后一个承诺先被解决,对于它的itemDone函数调用仍将在最后执行。 - undefined

3

我提供了一个没有使用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>


从OP的例子中可以看出,他们并不期望顺序执行,而这也可以通过使用Promise来实现。 - undefined

2

这个主题上有许多解决方案和方法来实现此目标!

但是,如果您需要使用 mapasync/await 来完成此操作,则可以按如下方式进行。

// Execution Starts
console.log("start")

// The Map will return promises
// the Execution will not go forward until all the promises are resolved.
await Promise.all(
    [1, 2, 3].map( async (item) => {
        await asyncFunction(item)
    })
)

// Will only run after all the items have resolved the asynchronous function. 
console.log("End")

输出结果会像这样!根据异步函数可能会有所不同。
start
2
3
1
end

注意:如果在 map 中使用 await,它将始终返回一个 Promise 数组。


如果你在一个map中使用await,它将始终返回一个promise数组。不,如果回调函数是一个异步函数,无论你是否使用await,它都会返回一个promise。 - undefined

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

解决方案很创新,但出现了一个错误 - “任务不是函数”。 - Deepam Gupta

0

0
这段代码对我很有用,感谢Nick Tomlin的回答:
$(document).ready(function(){

    let arr = ["a1", "a2", "a3","a1", "a2", "a3","a1", "a2", "a3","a1", "a2", "a3","a1", "a2", "a3"];
    let motherArray = new Array();


    let arrayPushDone = arr.reduce((prevPromise, currentValue)=>{
        return prevPromise.then(()=>{
            console.log("currentValue: ", currentValue);
            return new Promise((resolve) =>resolve(motherArray.push(currentValue)));
        });
    }, Promise.resolve());

    arrayPushDone.then(()=>console.log("array push done: ", motherArray));
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接