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!
Dan Andreasson
źródło
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();
Nick Tomlin
źródło
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.

Emil Reña Enriquez
źródło
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();
}
Adnene Belfodil
źródło
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);
});
Rsh
źródło
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');
}
Krzysztof Grzybek
źródło
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>

jackstrapp
źródło
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();
 });
Hardik Shimpi
źródło
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
Gabor
źródło
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

HamidReza Heydari
źródło
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)
})
Nilesh Pawar
źródło
-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();
azz
źródło
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()
}
molham556
źródło
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);
Tino Costa „El Nino”
źródło
To wygląda logicznie prosto
Zeal Murapa