Зворотний виклик після завершення асинхронних зворотних викликів для кожного


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 та ін.) Цей підхід гарантує, що всі елементи будуть оброблені, перш ніж викликати зворотний виклик "зроблено". Потрібно скористатися лічильником, який оновлюється при зворотному дзвінку. Залежно від значення показника параметр не дає однакової гарантії, оскільки не повертається порядок повернення асинхронних операцій.

Використання 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, тому ви можете просто поставити зворотний дзвінок після дзвінка, щоб передбачити:

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

  res.end();

31
Однак зауважте, що якщо в forEach є асинхронні речі (наприклад, ви перебираєте масив URL-адрес і здійснюєте HTTP GET на них), немає гарантії, що res.end буде називатися останнім.
AlexMA

Для того, щоб викликати
elkelk

2
@ Вануан Я оновив свою відповідь, щоб краще відповідати вашій досить важливій редагуванні :)
Нік Томлін

4
чому б не просто if(index === array.length - 1)і видалитиitemsProcessed
Амін Джафарі

5
@AminJafari, оскільки асинхронні дзвінки можуть не вирішуватися в тому порядку, в якому вони зареєстровані (скажімо, ви дзвоните на сервер, і він трохи зупиняється на 2-му дзвінку, але обробляє останній виклик штрафом). Останній асинхронний виклик міг вирішитись перед попередніми. Вимкнення зустрічного охоронця проти цього, оскільки всі зворотні виклики повинні запускатись незалежно від порядку впорядкування.
Нік Томлін

25

Якщо у вас виникли асинхронні функції, і ви хочете переконатися, що перед виконанням коду він закінчує свою задачу, ми завжди можемо використовувати можливість зворотного виклику.

Наприклад:

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

Примітка: functionAfterForEachце функція, яка повинна виконуватися після закінчення завдань foreach. asynchronous- це асинхронна функція, що виконується всередині foreach.


9
Це не працюватиме, оскільки порядок виконання асинхронних запитів не гарантовано. Останній запит на асинхронізацію може закінчитися перед іншими та виконати функціюAfterForEach (), перш ніж всі запити будуть виконані.
Rémy DAVID

@ RémyDAVID, так, у вас є пункт щодо порядку виконання, або я повинен сказати, як довго процес закінчився, однак, javascript є єдиним потоком, так що це працює в підсумку. І доказом є підтвердження цієї відповіді.
Еміль Ренья Енрікес

1
Я не надто впевнений, чому у вас так багато відгуків, але Ремі вірно. Ваш код взагалі не працюватиме, оскільки асинхронне означає, що будь-який запит може повернутися в будь-який час. Хоча JavaScript не є багатопоточним, ваш браузер є. Сильно, можу додати. Таким чином, він може зателефонувати будь-якому з ваших зворотних дзвінків у будь-який час у будь-якому порядку, залежно від того, коли надійде відповідь від сервера ...
Alexis Wilke

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();
}

У мене був подібний випадок у моєму коді 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);
});

1
Це, мабуть, єдине рішення. Чи використовує бібліотека async лічильники?
Вануан

1
Хоча інші рішення роблять цю роботу, це є найбільш переконливим, оскільки не потребує ланцюжків чи додаткової складності. KISS
азатар

Врахуйте також ситуацію, коли довжина масиву дорівнює нулю, у цьому випадку зворотний виклик ніколи не буде викликаний
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
Availabe in Node v10
Matt Swezey

2

Моє рішення без Обіцянки (це гарантує закінчення кожної дії до початку наступної):

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.
Судханшу Гаур


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

Рішення є інноваційним, але приходить помилка - "завдання не функція"
Геній

0

Я намагаюся простим способом вирішити це питання, поділіться ним із вами:

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 в Node js. Це може замінити кожну функцію або Code u want. Щасти


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()
}

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

Це виглядає логічно просто
Зор Мурапа
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.