Як я можу дочекатися набору асинхронних функцій зворотного виклику?


95

У мене є код, який виглядає приблизно так у javascript:

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

Після того, як ВСІ ці асинхронні виклики будуть зроблені, я хочу обчислити min для всіх масивів.

Як я можу чекати їх усіх?

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

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

Заздалегідь спасибі.


1
Під асинхронією ви маєте на увазі очікування завершення запиту Ajax?
Пітер Арон Зентай,

6
Зверніть увагу, while (not all are done) { }не спрацює. Поки ви зайняті очікуванням, жоден із ваших зворотних дзвінків не може працювати.
cHao

Так. Я чекаю асинхронного виклику зовнішнього API, щоб він повернувся, щоб він запускав методи зворотного виклику. Так cHao, я це зрозумів, ось чому я прошу про допомогу тут: 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 , тоді в неї вбудовані додаткові функції, які полегшують це:

 var doAjax = Promise.promisify(someAsync);
 var someData = [...]
 Promise.map(someData, doAjax).then(function(results) {
     // all ajax results here
 }, function(err) {
     // some error here
 });

4
@kba - Я б точно не назвав цю відповідь застарілою, оскільки всі методи все ще застосовуються, особливо якщо ви вже використовуєте jQuery для Ajax. Але я оновив його кількома способами, щоб включити рідні обіцянки.
jfriend00

У ці дні існує набагато чистіше рішення, яке навіть не потребує jquery. Я роблю це за допомогою FetchAPI та Promises
philx_x

@philx_x - Що ви робите щодо підтримки IE та Safari?
jfriend00

@ jfriend00 github зробив polyfill github.com/github/fetch . Або я не впевнений, чи Babel ще підтримує вибірку. babeljs.io
philx_x

@philx_x - Думав так. Вам потрібна бібліотека polyfill для того, щоб сьогодні використовувати fetch. Витягує трохи коментарів про те, як уникати бібліотеки Ajax. Витяг - це приємно, але до того, як можна використовувати його без поліфайла, ще багато років. Це навіть не в останній версії всіх браузерів. Зробіть, це насправді нічого не змінює у моїй відповіді. У мене був 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

Ви можете використовувати відкладений об’єкт jQuery разом із методом коли .

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що зазвичай означає, що ОП не хотів отримати відповідь jQuery.
jfriend00

8
@ jfriend00 Я не хотів винаходити колесо, коли воно вже було створене в jQuery
Пол

4
@Paul, тож замість цього вигадайте колесо, включаючи сміття на
40 кб,

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 - це шлях, але мені подобається ця відповідь, оскільки він ілюструє метод управління кодом Async у ситуаціях, коли поліфіл Promise більше, ніж JS, який його використовує!
Сукіма

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

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)
})
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.