javascript - Как последовательно вызвать асинхронную функцию с коллбеками?


6

Есть массив, для каждого элемента которого следует вызвать асинхронную функцию, возвращающую значение через коллбэк.

Однако, требуется вызвать их последовательно, а именно, следующий вызов надо совершать только тогда, когда завершилась обработка предыдущего.

Вот так получается одновременно:

function doSmth(x, callback) {
  setTimeout(callback, Math.random() * 100 | 0, null, x);
}

var data = [1, 2, 3, 4, 5, 6, 7, 8];

for (var x of data) {
  doSmth(x, function (err, res) {
    console.log(err || res);
  });
}
.as-console-wrapper.as-console-wrapper { max-height: 100vh }

  •  13
  •  2
  • 22 янв 2017 2017-01-22 06:46:50

2 ответа

4

В последних версиях языка есть механизмы, позволяющие писать более понятный код чем вариант с рекурсией.

Для начала, надо преобразовать функцию так, чтобы она возвращала обещание (Promise). Переписывать ее для этого не нужно: достаточно обернуть.

function doSmth1(x) {
     return new Promise((resolve, reject) => {
         doSmth(x, (err, result) => {
             if (err)
                 reject(err);
             else
                 resolve(result);
         });
     });
}

Также можно один раз написать функцию, которая будет оборачивать любую заданную (или взять ее из какой-нибудь библиотеки):

function promisify(f) {
    return function(...args) {
         return Promise((resolve, reject) => {
             f.call(this, ...args, (err, result) => {
                 if (err)
                      reject(err);
                 else
                      resolve(result);
             });
         });
    }
}

var doSmth1 = promisify(doSmth);

Это позволяет использовать асинхронные функции из стандарта ES2017:

async function foo() {
    var doSmth1 = promisify(doSmth);
    for (var x of data) {
        console.log(await doSmth1(x));
    }
}

Если по какой-то причине нет желания выделять целую функцию под асинхронный код - нет проблем вызвать анонимную функцию на месте:

(async () => {
    var doSmth1 = promisify(doSmth);
    for (var x of data) {
        console.log(await doSmth1(x));
    }
})();

PS когда вы пишите свои асинхронные функции, имеет смысл делать их "двойного назначения" - одновременно принимающими callback и возвращающими Promise. Это не трудно, но способы написания таких функций выходят за рамки ответа.

@Qwertiy да нет, это именно что ответ на вопрос "как последовательно вызвать асинхронную функцию с коллбеками?" - обернуть в промизы и воспользоваться новыми средствами языка. Цель ответа - показать что новые средства языка прикладной код не усложняют - а упрощают, и как их использовать в легаси-окружении. — 25 янв 20172017-01-25 10:18:41.000000
Добавил бы сниппет в конец... А вообще, по-моему, это стоило отдельным вопросом с ответом делать. — 25 янв 20172017-01-25 10:16:21.000000
6

Надо делать вызов внутри коллбека. Для этого придётся переписать цикл на рекурсию (ну или не совсем рекурсию):

function doSmth(x, callback) {
  setTimeout(callback, Math.random() * 100 | 0, null, x);
}

var data = [1, 2, 3, 4, 5, 6, 7, 8];

(function go(i) { // <================== рекурсивная функция вместо цикла
  if (i >= data.length) {
    return; // <======================== выход, когда массив закончился
  }
  
  doSmth(data[i], function (err, res) {
    console.log(err || res);
    
    go(i + 1); // <===================== рекурсивный вызов из коллбэка
  });
})(0);
.as-console-wrapper.as-console-wrapper { max-height: 100vh }

И остаётся ещё один момент - обычно нам бы надо узнать, когда завершилась обработка. Для этого функция go вместо return может вызвать другой коллбэк из вызывающей функции:

function doSmth(x, callback) {
  setTimeout(callback, Math.random() * 100 | 0, null, x);
}

function process(data, callback) {
  (function go(i) { // <================== рекурсивная функция вместо цикла
    if (i >= data.length) {
      return callback(null, null); // <=== return позволяет избежать рекурсивного вызова
    }

    doSmth(data[i], function (err, res) {
      if (err) {
        return callback(err, null); // <== прекращаем дальнейшую обработку
      }
    
      console.log(res);

      go(i + 1); // <===================== рекурсивный вызов из коллбэка
    });
  })(0);
}

var data = [1, 2, 3, 4, 5, 6, 7, 8];

process(data, function (err, res) {
  console.log(err || "Готово!");
});
.as-console-wrapper.as-console-wrapper { max-height: 100vh }

  • 2 янв 2018 2018-01-02 16:55:32
@Grundy, во-первых, его ещё надо найти. Во-вторых, на faq он не тянет. Ну и наконец, я хотел описать именно выпрямление цикла в случае, когда функция с коллбеками уже есть и её трогать нельзя и не надо. — 21 янв 20172017-01-21 17:02:49.000000
А вот этот? — 21 янв 20172017-01-21 16:59:58.000000
@Grundy, помню вот этот, но он про другое, хотя и связан с этим. — 21 янв 20172017-01-21 16:58:07.000000
я уверен уже было несколько вопросов с последовательным выполнением асинхронных функций — 21 янв 20172017-01-21 16:55:28.000000