Обратный вызов после завершения всех асинхронных обратных вызовов forEach

245

Как следует из названия. Как мне это сделать?

Я хочу позвонить 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обратного вызова и обратный вызов!
Вануан
22
Это настоящий позор, когда что-то такое простое требует такой большой борьбы в JavaScript.
Али

Ответы:

410

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'));

Использование асинхронной библиотеки

Есть и другие асинхронные библиотеки, async является наиболее популярным, которые обеспечивают механизмы , чтобы выразить то , что вы хотите.

редактировать

Основная часть вопроса была отредактирована для удаления ранее синхронного примера кода, поэтому я обновил свой ответ, чтобы уточнить. В исходном примере для моделирования асинхронного поведения использовался синхронный код, поэтому применялось следующее:

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
Чтобы запустить обратный вызов после выполнения асинхронного действия в цикле, вы можете использовать каждый метод асинхронной утилиты: github.com/caolan/async#each
elkelk
2
@Vanuan Я обновил свой ответ, чтобы он лучше соответствовал вашему довольно значимому редактированию :)
Ник Томлин
4
почему бы просто не if(index === array.length - 1)убратьitemsProcessed
Амин Джафари
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 является однопоточным, так что в конечном итоге это работает. И доказательством является ответ, полученный этим ответом.
Эмиль Ренья Энрикес
1
Я не слишком уверен, почему у вас так много голосов, но Реми прав. Ваш код не будет работать вообще, поскольку асинхронный означает, что любой запрос может вернуться в любое время. Хотя JavaScript не многопоточный, ваш браузер. Сильно, я мог бы добавить. Таким образом, он может вызывать любой из ваших обратных вызовов в любое время в любом порядке, в зависимости от того, когда ответ получен с сервера ...
Алексис Уилк
2
да, это ответ совершенно неправильно. Если я запускаю 10 загрузок параллельно, это почти гарантирует, что последняя загрузка закончится раньше остальных и, таким образом, завершит выполнение.
knrdk
Я бы посоветовал вам использовать счетчик для увеличения числа выполненных асинхронных задач и сопоставления его с длиной массива вместо индекса. Количество голосов не имеет ничего общего с доказательством правильности ответа.
Алекс
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, и этот ответ помог мне. Хотя ответ @Emil Reña Enriquez также помог мне, но я считаю, что это более точный и простой ответ на эту проблему.
омостан
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, обратный вызов будет вызван после завершения первой итерации, в то время как первый элемент еще не завершен!

Я думаю, что для решения этой проблемы без использования внешних библиотек, таких как 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);
});
Rsh
источник
1
Это, наверное, единственное решение. Асинхронная библиотека также использует счетчики?
Вануан
1
Хотя другие решения выполняют свою работу, это наиболее убедительно, поскольку не требует цепочки или дополнительной сложности. ПОЦЕЛУЙ
азатар
Обратите также внимание на ситуацию, когда длина массива равна нулю, в этом случае обратный вызов никогда не будет вызван
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
Мэтт
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>

jackstrapp
источник
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.
Судханшу Гаур
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
Gabor
источник
Решение новаторское, но приходит ошибка - «задача не функция»
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является функцией библиотеки mssql в узле js Это может заменить каждую функцию или код, который вы хотите. Удачи

ХамидРеза Хейдари
источник
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
Нет. ОП подчеркнул, что асинхронная логика будет выполняться для каждой итерации. res.writeНЕ является асинхронной операцией, поэтому ваш код не будет работать.
Джим Г.
-2

Простое решение будет как следовать

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

["a", "b", "c"].forEach(function(item, index, array){
    //code here
    if(i == array.length -1)
    callback()
}
molham556
источник
3
Не работает для асинхронного кода, который является полной предпосылкой вопроса.
августа
-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);
Тино Коста 'Эль Нино'
источник
Это выглядит логически просто
Рвение Мурапа