Как мне дождаться набора асинхронных функций обратного вызова?

95

У меня есть код в javascript, который выглядит примерно так:

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

После выполнения ВСЕХ этих асинхронных вызовов я хочу вычислить минимум по всем массивам.

Как я могу дождаться их всех?

Моя единственная идея прямо сейчас - создать массив логических значений с именем done и установить для done [i] значение true в i-й функции обратного вызова, а затем сказать while (не все выполнено) {}

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

Заранее спасибо.

кодеры
источник
1
Под async вы имеете в виду ожидание завершения запроса Ajax?
Питер Арон Зентаи
6
Обратите внимание, while (not all are done) { }не сработает. Пока вы заняты ожиданием, ни один из ваших обратных вызовов не может выполняться.
cHao
Да. Я жду возврата асинхронного вызова внешнего API, чтобы он запустил методы обратного вызова. Да, хао, я это понял, поэтому и прошу здесь помощи: D
codersarepeople
Вы можете попробовать это: github.com/caolan/async Очень хороший набор служебных функций async.
Пол Грейсон

Ответы:

191

Вы не очень подробно описали свой код, поэтому я придумываю сценарий. Допустим, у вас есть 10 вызовов ajax, и вы хотите накопить результаты этих 10 вызовов ajax, а затем, когда все они будут выполнены, вы хотите что-то сделать. Вы можете сделать это так, накапливая данные в массиве и отслеживая, когда закончился последний:

Ручной счетчик

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

Примечание: здесь важна обработка ошибок (не показана, потому что она зависит от того, как вы выполняете свои вызовы ajax). Вы захотите подумать о том, как вы собираетесь обрабатывать случай, когда один вызов ajax никогда не завершается, либо с ошибкой, либо застревает на долгое время, либо истекает по истечении длительного времени.


jQuery обещает

Добавляем к моему ответу в 2014 году. В наши дни обещания часто используются для решения этого типа проблемы, поскольку jQuery $.ajax()уже возвращает обещание и $.when()сообщит вам, когда все группы обещаний будут решены, и соберет для вас результаты возврата:

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

Стандартные обещания ES6

Как указано в ответе kba : если у вас есть среда со встроенными встроенными обещаниями (современный браузер или node.js, или с использованием babeljs transpile или с использованием полифилла обещаний), вы можете использовать обещания, указанные в ES6. См. Эту таблицу для поддержки браузера. Обещания поддерживаются практически во всех текущих браузерах, кроме IE.

Если doAjax()возвращает обещание, вы можете сделать это:

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

Если вам нужно превратить асинхронную операцию без обещания в операцию, которая возвращает обещание, вы можете «обещать» ее следующим образом:

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

И затем используйте шаблон выше:

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

Если вы используете библиотеку с более широким набором функций, такую ​​как библиотека обещаний Bluebird , то в нее встроены некоторые дополнительные функции, упрощающие задачу:

 var doAjax = Promise.promisify(someAsync);
 var someData = [...]
 Promise.map(someData, doAjax).then(function(results) {
     // all ajax results here
 }, function(err) {
     // some error here
 });
jfriend00
источник
4
@kba - я бы точно не назвал этот ответ устаревшим, поскольку все методы все еще применимы, особенно если вы уже используете jQuery для Ajax. Но я обновил его несколькими способами, включив собственные обещания.
jfriend00
В эти дни существует гораздо более чистое решение, которое даже не требует jquery. Я делаю это с помощью FetchAPI и Promises
philx_x
@philx_x - Что вы делаете с поддержкой IE и Safari?
jfriend00
@ jfriend00 github сделал полифилл github.com/github/fetch . Или я не уверен, поддерживает ли babel выборку. babeljs.io
philx_x
@philx_x - Так и думал. В настоящее время вам нужна библиотека polyfill, чтобы использовать fetch. Немного отвлечется от вашего комментария об отказе от библиотеки ajax. Fetch хорош, но до того, как использовать его без полифилла, еще далеко. Это еще не во всех последних версиях браузеров. Да, это ничего не меняет в моем ответе. У меня был doAjax()один из вариантов, который возвращает обещание. То же, что и fetch().
jfriend00
17

Регистрация с 2015 года: теперь у нас есть встроенные обещания в самых последних версиях браузеров (Edge 12, Firefox 40, Chrome 43, Safari 8, Opera 32 и браузер Android 4.4.4 и iOS Safari 8.4, но не в Internet Explorer, Opera Mini и более ранних версиях). Android).

Если мы хотим выполнить 10 асинхронных действий и получить уведомление, когда все они будут завершены, мы можем использовать нативные Promise.all, без каких-либо внешних библиотек:

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);
});
кба
источник
2
Promises.all()должно быть Promise.all().
jfriend00
1
В вашем ответе также необходимо указать, какие браузеры вы можете использовать, Promise.all()в которых нет текущих версий IE.
jfriend00
10

Вы можете использовать объект Deferred jQuery вместе с методом when .

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
});
Павел
источник
7
Вопрос не был помечен, jQueryчто обычно означает, что OP не хотел ответа jQuery.
jfriend00
8
@ jfriend00 Я не хотел изобретать велосипед, когда он уже был создан в jQuery
Пол
4
@Paul, так что лучше не изобретать колесо, включая 40кб мусора, чтобы сделать что-то простое (отложенные)
Raynos
2
Но не все могут или хотят использовать jQuery, и обычай здесь, в SO, заключается в том, что вы указываете это, помечая свой вопрос с помощью jQuery или нет.
jfriend00
4
Вызов $ .when в этом примере неверен. Чтобы дождаться массива отложенных / обещаний, вам нужно использовать $ .when.apply ($, promises) .then (function () {/ * do stuff * /}).
danw
9

Вы можете эмулировать это так:

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

то каждый асинхронный вызов делает следующее:

countDownLatch.count++;

а в каждом асинхронном вызове в конце метода вы добавляете эту строку:

countDownLatch.check();

Другими словами, вы имитируете обратный отсчет с защелкой.

Евгений Ретунский
источник
В 99% всех случаев использования Promise - лучший способ, но мне нравится этот ответ, потому что он иллюстрирует метод управления асинхронным кодом в ситуациях, когда полифил Promise больше, чем JS, который его использует!
Sukima
6

На мой взгляд, это самый изящный способ.

Promise.all

FetchAPI

(у меня по какой-то причине Array.map не работает внутри функций .then. Но вы можете использовать .forEach и [] .concat () или что-то подобное)

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
источник
1
Думаю это должно быть return responses.map(response => { return response.json(); }), или return responses.map(response => response.json()).
1

Используйте библиотеку потока управления, например 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)
})
Райнос
источник