Oddzwonienie po zakończeniu wszystkich asynchronicznych wywołań forEach


245

Jak sugeruje tytuł. Jak mam to zrobic?

Chcę zadzwonić whenAllDone()po przejściu pętli forEach przez każdy element i wykonaniu asynchronicznego przetwarzania.

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

Czy jest możliwe, aby działało w ten sposób? Kiedy drugim argumentem forEach jest funkcja zwrotna, która jest uruchamiana po przejściu wszystkich iteracji?

Oczekiwany wynik:

3 done
1 done
2 done
All done!

13
Byłoby miło, gdyby standardowa forEachmetoda tablicowa miała doneparametr allDonewywołania zwrotnego i wywołanie zwrotne!
— Vanuan,

22
To wielka szkoda, że ​​coś tak prostego wymaga tyle zapasów w JavaScript.
— Ali

Odpowiedzi:


410

Array.forEach nie zapewnia tej subtelności (och, gdyby tak było), ale istnieje kilka sposobów na osiągnięcie tego, co chcesz:

Za pomocą prostego licznika

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

var itemsProcessed = 0;

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

(dzięki @vanuan i innym) Takie podejście gwarantuje, że wszystkie elementy zostaną przetworzone przed wywołaniem wywołania zwrotnego „gotowe”. Musisz użyć licznika, który jest aktualizowany w wywołaniu zwrotnym. W zależności od wartości parametru indeksu nie zapewnia tej samej gwarancji, ponieważ kolejność zwrotu operacji asynchronicznych nie jest gwarantowana.

Korzystanie z obietnic ES6

(w starszych przeglądarkach można użyć biblioteki obietnic):

  1. Przetwarzaj wszystkie żądania gwarantujące synchroniczne wykonanie (np. 1, 2, 3, 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. Przetwarzaj wszystkie żądania asynchroniczne bez wykonania „synchronicznego” (2 może zakończyć się szybciej niż 1)

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

Korzystanie z biblioteki asynchronicznej

Istnieją inne biblioteki asynchroniczne , z których najpopularniejsze to asynchroniczne , które zapewniają mechanizmy wyrażania tego, co chcesz.

Edytować

Treść pytania została poddana edycji w celu usunięcia wcześniej zsynchronizowanego przykładowego kodu, więc zaktualizowałem swoją odpowiedź, aby wyjaśnić. W oryginalnym przykładzie wykorzystano synchroniczny kod podobny do modelowania zachowania asynchronicznego, dlatego zastosowano następujące zasady:

array.forEachjest zsynchronizowany i tak też jest res.write, więc możesz po prostu oddzwonić po połączeniu, aby foreach:

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

  res.end();

31
Pamiętaj jednak, że jeśli wewnątrz forEach znajdują się elementy asynchroniczne (np. Przeglądasz tablicę adresów URL i wykonujesz na nich HTTP GET), nie ma gwarancji, że res.end zostanie wywołany jako ostatni.
— AlexMA

Aby uruchomić wywołanie zwrotne po wykonaniu akcji asynchronicznej w pętli, możesz użyć każdej metody narzędzia asynchronicznego: github.com/caolan/async#each
— elkelk

2
@Vanuan Zaktualizowałem moją odpowiedź, aby lepiej pasowała do twojej dość znaczącej edycji :)
— Nick Tomlin

4
dlaczego nie tylko if(index === array.length - 1)i usunąćitemsProcessed
— Amin Jafari,

5
@AminJafari, ponieważ wywołania asynchroniczne mogą nie być przetwarzane w dokładnie takiej kolejności, w jakiej zostały zarejestrowane (powiedzmy, że dzwonisz do serwera, a przy drugim wywołaniu nieznacznie się zatrzymuje, ale ostatnie połączenie jest w porządku). Ostatnie asynchroniczne wywołanie może zostać rozwiązane przed poprzednimi. Mutowanie kontrataków chroni przed tym, ponieważ wszystkie oddzwaniania muszą strzelać bez względu na kolejność ich rozpatrywania.
— Nick Tomlin,

25

Jeśli napotkasz funkcje asynchroniczne i chcesz się upewnić, że przed wykonaniem kodu zakończy on swoje zadanie, zawsze możemy skorzystać z funkcji wywołania zwrotnego.

Na przykład:

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

Uwaga: functionAfterForEachto funkcja, która ma być wykonana po zakończeniu wszystkich zadań. asynchronousjest funkcją asynchroniczną wykonywaną wewnątrz foreach.


9
To nie zadziała, ponieważ kolejność wykonywania żądań asynchronicznych nie jest oczywista. Ostatnie żądanie asynchroniczne może zakończyć się przed innymi i wykonać funkcjęAfterForEach () przed wykonaniem wszystkich żądań.
— Rémy DAVID

@ RémyDAVID tak, masz rację co do kolejności wykonywania, czy powiem, jak długo proces jest zakończony, jednak javascript jest jednowątkowy, więc w końcu to działa. A dowodem jest głosowanie za otrzymaniem tej odpowiedzi.
— Emil Reña Enriquez

1
Nie jestem zbyt pewien, dlaczego masz tyle głosów poparcia, ale Rémi ma rację. Twój kod w ogóle nie będzie działał, ponieważ asynchroniczny oznacza, że ​​każde żądanie może zostać zwrócone w dowolnym momencie. Chociaż JavaScript nie jest wielowątkowy, Twoja przeglądarka to. Ciężko mogę dodać. A zatem może wywołać jeden z twoich wywołania zwrotne w dowolnym momencie w dowolnej kolejności, w zależności od przypadku odpowiedź jest odbierana z serwera ...
— Alexis Wilke

2
tak, to odpowiedź jest całkowicie błędna. Jeśli uruchomię 10 pobrań równolegle, to prawie gwarantuje, że ostatnie pobieranie zakończy się przed resztą i tym samym zakończy wykonywanie.
— knrdk

Sugeruję użycie licznika do zwiększenia liczby wykonanych zadań asynchronicznych i dopasowania ich do długości tablicy zamiast indeksu. Liczba głosów pozytywnych nie ma nic wspólnego z dowodem poprawności odpowiedzi.
— Alex

17

Mam nadzieję, że to rozwiąże problem, zwykle pracuję z tym, gdy muszę wykonać forEach z asynchronicznymi zadaniami wewnątrz.

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

z

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

Miałem podobny problem w kodzie Angular 9 i ta odpowiedź pomogła mi. Chociaż odpowiedź @Emil Reña Enriquez również działała dla mnie, ale uważam, że jest to dokładniejsza i prostsza odpowiedź na ten problem.
— omostan

17

Dziwne, ile błędnych odpowiedzi zostało podanych w przypadku asynchronicznym ! Można po prostu pokazać, że sprawdzanie indeksu nie zapewnia oczekiwanego zachowania:

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

wynik:

4000 started
2000 started
1: 2000
0: 4000

Jeśli sprawdzimy index === array.length - 1, oddzwonienie zostanie wywołane po zakończeniu pierwszej iteracji, podczas gdy pierwszy element jest nadal w toku!

Myślę, że aby rozwiązać ten problem bez korzystania z zewnętrznych bibliotek, takich jak asynchronizacja, najlepiej jest zapisać długość listy i zmniejszyć ją po każdej iteracji. Ponieważ jest tylko jeden wątek, jesteśmy pewni, że nie ma szans na warunki wyścigowe.

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
To chyba jedyne rozwiązanie. Czy biblioteka asynchroniczna używa również liczników?
— Vanuan,

1
Chociaż inne rozwiązania spełniają swoje zadanie, jest to najbardziej przekonujące, ponieważ nie wymaga łączenia łańcuchów ani dodatkowej złożoności. KISS
— azatar

Proszę również wziąć pod uwagę sytuację, gdy długość tablicy wynosi zero, w tym przypadku wywołanie zwrotne nigdy nie zostanie wywołane
— Saeed Ir

6

W ES2018 możesz używać iteratorów asynchronicznych:

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
Dostępne w Node v10
— Matt Swezey,

2

Moje rozwiązanie bez obietnicy (zapewnia to, że każda akcja zostanie zakończona przed rozpoczęciem kolejnej):

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
To nie zadziała, ponieważ jeśli będziesz mieć operację asynchroniczną wewnątrz foreach.
— Sudhanshu Gaur


0

Moje rozwiązanie:

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

Przykład:

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

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

// out: a b c done

Rozwiązanie jest innowacyjne, ale nadchodzi błąd - „zadanie nie jest funkcją”
— Geniusz

0

Wypróbowuję Easy Way, aby go rozwiązać, podziel się nim z tobą:

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

requestjest funkcją biblioteki mssql w węźle js. Może to zastąpić każdą funkcję lub dowolny kod. Powodzenia


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

Nie powinieneś potrzebować oddzwaniania do iteracji po liście. Po prostu dodaj end()połączenie po pętli.

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

3
Nie. OP podkreślił, że logika asynchroniczna będzie wykonywana dla każdej iteracji. res.writeNIE jest operacją asynchroniczną, więc kod nie będzie działał.
— Jim G.

-2

Proste rozwiązanie byłoby następujące

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

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

3
Nie działa w przypadku kodu asynchronicznego, który stanowi całą przesłankę pytania.
— grg

-3

Co powiesz na setInterval, aby sprawdzić pełną liczbę iteracji, daje gwarancję. Nie jestem pewien, czy nie przeładuje on zakresu, ale używam go i wydaje się, że to on

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

To wygląda logicznie prosto
— Zeal Murapa
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.