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

73
07 июля 2021, 20:20

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

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

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

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 }

Answer 1

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

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 }

Answer 2

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

Для начала, надо преобразовать функцию так, чтобы она возвращала обещание (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. Это не трудно, но способы написания таких функций выходят за рамки ответа.

READ ALSO
В background script замораживаются таймеры

В background script замораживаются таймеры

Пишу расширение для Chrome и таймеры, в том числе не высокочастотные (более 1 мин) со временем просто перестают срабатывать в background скрипте расширения

87
ASP.NET Core. Не устанавливается Webpack 4

ASP.NET Core. Не устанавливается Webpack 4

Всем приветПишу SPA-приложение на ASP

83
Как управлять адаптивностью на самой странице без перезагрузки?

Как управлять адаптивностью на самой странице без перезагрузки?

Есть расширение для Мозиллы и Хрома Есть 2 файла CSS, назовем их style1css и style2

79
Автопрокрутка слайдов

Автопрокрутка слайдов

https://codepenio/JavaScriptJunkie/pen/WgRBxw

123