Jak mogę czekać na zestaw asynchronicznych funkcji zwrotnych?

95

Mam kod, który wygląda mniej więcej tak w javascript:

forloop {
    //async call, returns an array to its callback
}

Po wykonaniu WSZYSTKICH wywołań asynchronicznych chcę obliczyć min dla wszystkich tablic.

Jak mogę czekać na nich wszystkich?

Moim jedynym pomysłem w tej chwili jest mieć tablicę wartości logicznych o nazwie done i ustawić done [i] na true w i-tej funkcji zwrotnej, a następnie powiedzieć while (nie wszystkie są gotowe) {}

edit: Przypuszczam, że jednym możliwym, ale brzydkim rozwiązaniem byłoby edytowanie gotowej tablicy w każdym wywołaniu zwrotnym, a następnie wywołanie metody, jeśli wszystkie inne gotowe są ustawione z każdego wywołania zwrotnego, a zatem ostatnie wywołanie zwrotne do zakończenia wywoła metodę kontynuacji.

Z góry dziękuję.

codersarepeople
źródło
1
Czy w trybie asynchronicznym masz na myśli czekanie na zakończenie żądania Ajax?
Peter Aron Zentai
6
Uwaga, while (not all are done) { }nie zadziała. Gdy jesteś zajęty czekaniem, żadne z Twoich oddzwonień nie może działać.
cHao
Tak. Czekam na wywołanie asynchroniczne do zewnętrznego interfejsu API, aby powrócić, aby uruchomić metody wywołania zwrotnego. Tak, cHao, zdałem sobie z tego sprawę, dlatego proszę o pomoc tutaj: D
codersarepeople
Możesz spróbować tego: github.com/caolan/async Bardzo ładny zestaw funkcji narzędziowych asynchronicznych.
Paul Greyson,

Odpowiedzi:

191

Nie byłeś zbyt szczegółowy w swoim kodzie, więc wymyślę scenariusz. Powiedzmy, że masz 10 wywołań Ajax i chcesz zebrać wyniki z tych 10 wywołań AJAX, a gdy wszystkie się zakończą, chcesz coś zrobić. Możesz to zrobić w ten sposób, gromadząc dane w tablicy i śledząc, kiedy zakończy się ostatnia:

Licznik ręczny

var ajaxCallsRemaining = 10;
var returnedData = [];

for (var i = 0; i < 10; i++) {
    doAjax(whatever, function(response) {
        // success handler from the ajax call

        // save response
        returnedData.push(response);

        // see if we're done with the last ajax call
        --ajaxCallsRemaining;
        if (ajaxCallsRemaining <= 0) {
            // all data is here now
            // look through the returnedData and do whatever processing 
            // you want on it right here
        }
    });
}

Uwaga: obsługa błędów jest tutaj ważna (nie jest wyświetlana, ponieważ jest specyficzna dla sposobu wykonywania połączeń Ajax). Będziesz chciał pomyśleć o tym, jak poradzisz sobie z przypadkiem, gdy jedno wywołanie ajax nigdy się nie kończy, albo z błędem, albo utknie na długi czas lub po długim czasie upłynie.


jQuery Promises

Dodając do mojej odpowiedzi w 2014 roku. Obecnie obietnice są często używane do rozwiązywania tego typu problemów, ponieważ jQuery $.ajax()już zwraca obietnicę i $.when()poinformuje Cię, kiedy grupa obietnic zostanie rozwiązana, i zbierze wyniki zwrotu:

var promises = [];
for (var i = 0; i < 10; i++) {
    promises.push($.ajax(...));
}
$.when.apply($, promises).then(function() {
    // returned data is in arguments[0][0], arguments[1][0], ... arguments[9][0]
    // you can process it here
}, function() {
    // error occurred
});

Standardowe obietnice ES6

Jak określono w odpowiedzi kba : jeśli masz środowisko z wbudowanymi natywnymi obietnicami (nowoczesna przeglądarka lub node.js lub używając babeljs transpile lub używając wypełnienia obietnicy), możesz użyć obietnic określonych w ES6. Zobacz tę tabelę, aby uzyskać informacje o obsłudze przeglądarek. Obietnice są obsługiwane w prawie wszystkich obecnych przeglądarkach, z wyjątkiem IE.

Jeśli doAjax()zwróci obietnicę, możesz to zrobić:

var promises = [];
for (var i = 0; i < 10; i++) {
    promises.push(doAjax(...));
}
Promise.all(promises).then(function() {
    // returned data is in arguments[0], arguments[1], ... arguments[n]
    // you can process it here
}, function(err) {
    // error occurred
});

Jeśli chcesz wykonać operację asynchroniczną niezwiązaną z obietnicą w taką, która zwraca obietnicę, możesz ją „obiecać” w następujący sposób:

function doAjax(...) {
    return new Promise(function(resolve, reject) {
        someAsyncOperation(..., function(err, result) {
            if (err) return reject(err);
            resolve(result);
        });
    });
}

Następnie użyj powyższego wzoru:

var promises = [];
for (var i = 0; i < 10; i++) {
    promises.push(doAjax(...));
}
Promise.all(promises).then(function() {
    // returned data is in arguments[0], arguments[1], ... arguments[n]
    // you can process it here
}, function(err) {
    // error occurred
});

Bluebird obiecuje

Jeśli używasz bogatszej w funkcje biblioteki, takiej jak biblioteka obietnic Bluebird , ma ona wbudowane dodatkowe funkcje, które to ułatwiają:

 var doAjax = Promise.promisify(someAsync);
 var someData = [...]
 Promise.map(someData, doAjax).then(function(results) {
     // all ajax results here
 }, function(err) {
     // some error here
 });
jfriend00
źródło
4
@kba - nie nazwałbym tej odpowiedzi przestarzałą, ponieważ wszystkie techniki nadal mają zastosowanie, szczególnie jeśli używasz już jQuery dla Ajax. Ale zaktualizowałem go na kilka sposobów, aby uwzględnić natywne obietnice.
jfriend00
Obecnie istnieje znacznie czystsze rozwiązanie, które nawet nie wymaga jquery. Robię to z FetchAPI i Promises
philx_x,
@philx_x - Co robisz w związku z obsługą IE i Safari?
jfriend00
@ jfriend00 github utworzył polifill github.com/github/fetch . Albo nie jestem jeszcze pewien, czy babel obsługuje pobieranie. babeljs.io
philx_x
@philx_x - Tak myślałem. Aby korzystać z funkcji pobierania, potrzebujesz obecnie biblioteki polyfill. Wyciąga trochę powietrza z twojego komentarza na temat unikania biblioteki Ajax. Fetch jest fajny, ale minęły lata, zanim będzie można go używać bez polyfill. Nie ma go jeszcze w najnowszej wersji wszystkich przeglądarek. Tak naprawdę to niczego nie zmienia w mojej odpowiedzi. Miałem doAjax()opcję, która zwraca obietnicę jako jedną z opcji. To samo co fetch().
jfriend00
17

Meldowanie od 2015 roku: mamy teraz natywne obietnice w najnowszej przeglądarce (Edge 12, Firefox 40, Chrome 43, Safari 8, Opera 32 i Android 4.4.4 i iOS Safari 8.4, ale nie Internet Explorer, Opera Mini i starsze wersje systemu Android).

Jeśli chcemy wykonać 10 akcji asynchronicznych i otrzymać powiadomienie o zakończeniu wszystkich, możemy skorzystać z natywnej Promise.all, bez żadnych zewnętrznych bibliotek:

function asyncAction(i) {
    return new Promise(function(resolve, reject) {
        var result = calculateResult();
        if (result.hasError()) {
            return reject(result.error);
        }
        return resolve(result);
    });
}

var promises = [];
for (var i=0; i < 10; i++) {
    promises.push(asyncAction(i));
}

Promise.all(promises).then(function AcceptHandler(results) {
    handleResults(results),
}, function ErrorHandler(error) {
    handleError(error);
});
kba
źródło
2
Promises.all()powinno być Promise.all().
jfriend00
1
Twoja odpowiedź musi również odnosić się do przeglądarek, z których możesz korzystać, Promise.all()co nie obejmuje aktualnych wersji IE.
jfriend00
10

Można użyć jQuery odroczony obiektu wraz z , gdy metody.

deferredArray = [];
forloop {
    deferred = new $.Deferred();
    ajaxCall(function() {
      deferred.resolve();
    }
    deferredArray.push(deferred);
}

$.when(deferredArray, function() {
  //this code is called after all the ajax calls are done
});
Paweł
źródło
7
Pytanie nie zostało oznaczone, jQueryco zwykle oznacza, że ​​operator operacyjny nie chciał odpowiedzi jQuery.
jfriend00
8
@ jfriend00 Nie chciałem wymyślać na nowo koła, które zostało już utworzone w jQuery
Paul
4
@Paul, więc zamiast wymyślić ponownie koło, w tym 40kb śmieci, aby zrobić coś prostego (odroczone)
Raynos
2
Ale nie każdy może lub chce używać jQuery, a tutaj w SO jest to zwyczaj, że wskazujesz to, czy oznaczasz swoje pytanie jQuery, czy nie.
jfriend00
4
$ .When call to ten przykład jest niepoprawny. Aby czekać na tablicę odroczonych / obietnic, musisz użyć $ .when.apply ($, promises) .then (function () {/ * do stuff * /}).
danw
9

Możesz to naśladować w ten sposób:

  countDownLatch = {
     count: 0,
     check: function() {
         this.count--;
         if (this.count == 0) this.calculate();
     },
     calculate: function() {...}
  };

to każde wywołanie asynchroniczne robi to:

countDownLatch.count++;

podczas gdy w każdym wywołaniu asynchronicznym na końcu metody dodajesz tę linię:

countDownLatch.check();

Innymi słowy, emulujesz funkcję odliczania w dół.

Eugene Retunsky
źródło
W 99% wszystkich przypadków użycia Promise jest drogą do zrobienia, ale podoba mi się ta odpowiedź, ponieważ ilustruje metodę zarządzania kodem Async w sytuacjach, gdy wypełnienie Promise jest większe niż JS, który go używa!
Sukima
6

To moim zdaniem najbardziej zgrabny sposób.

Promise.all

FetchAPI

(z jakiegoś powodu Array.map nie działa dla mnie wewnątrz funkcji .then. Ale możesz użyć .forEach i [] .concat () lub czegoś podobnego)

Promise.all([
  fetch('/user/4'),
  fetch('/user/5'),
  fetch('/user/6'),
  fetch('/user/7'),
  fetch('/user/8')
]).then(responses => {
  return responses.map(response => {response.json()})
}).then((values) => {
  console.log(values);
})
philx_x
źródło
1
Myślę, że tak musi być return responses.map(response => { return response.json(); }), lub return responses.map(response => response.json()).
1

Użyj biblioteki przepływu sterowania, takiej jak after

after.map(array, function (value, done) {
    // do something async
    setTimeout(function () {
        // do something with the value
        done(null, value * 2)
    }, 10)
}, function (err, mappedArray) {
    // all done, continue here
    console.log(mappedArray)
})
Raynos
źródło