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

126
04 сентября 2019, 10:30

Как написать простой, понятный, легко обслуживаемый код, который запускает последовательно несколько асинхронных функций в javascript/jQuery? (когда отработает одна, должна запускаться другая)

Следующий пример иллюстриует мой вопрос:

function f1(){ setTimeout( function(){ console.log(1); }, 30); }
function f2(){ setTimeout( function(){ console.log(2); }, 20); }
function f3(){ setTimeout( function(){ console.log(3); }, 10); }
f1(); f2(); f3(); 

на выходе 3 2 1 как сделать что-бы выдавало 1 2 3 ?

Желательно без коллбеков - т.к. если надо запустить последовательно больше двух функций это уже тяжело читать. Многое говорит о том что возможно решение с помощью обьекта $.Deferred, но пока не видел разумного варианта.

Подобный вопрос задавался не раз, но я почему-то не нашел ответа который бы меня устроил.

Answer 1

Если вы хотите использовать Обещания (Promise), то для начала вам нужно модифицировать ваши функции так, чтобы они возвращали Обещания. Например, первая из ваших функций будет иметь вид:

function f1() {
    return new Promise(function(resolve){
        setTimeout(function() {
            console.log(1);
            resolve();
        }, 30);
    });
}

Остальные функции преобразуются аналогичным образом.

Теперь у вас есть три функции (f1, f2, f3), которые возвращают Обещания и вы хотите выполнить их последовательно. Если вы не используете библиотек, вроде Bluebird, то вам придется реализовать очередь вызова Обещаний вручную. Это не так сложно, как кажется:

// Аргумент "deeds" - это массив функций, которые должны выполняться
// последовательно. При этом, каждая функция должна возвращать
// Обещание (Promise).
var seqRunner = function(deeds) {
    return deeds.reduce(function(p, deed) {
        return p.then(function() {
            // Выполняем следующую функцию только после того, как отработала
            // предыдущая.
            return deed();
        });
    }, Promise.resolve()); // Инициализируем очередь выполнения.
}

А пользоваться этой очередью нужно вот так:

seqRunner([f1, f2, f3]).then(function() {
    console.log('Done!');
});

А вот и JSFiddle с рабочим примером.

Замечание:

Если у вас заранее известное, небольшое число функций, то можно вообще обойтись без функции seqRunner и связывать функции вручную:

f1().then(function() {
    return f2();
}).then(function() {
    return f3();
}).then(function() {
    console.log('Done!');
});
Answer 2

Альтернативный вариант, сделать обертку над setTimeout возвращающую Promise, например так:

function delay(timeout){
    return new Promise(function(r){
        setTimeout(r,timeout);
    });
}

Теперь код из вопроса может выглядеть следующим образом:

function delay(timeout) { 
  return new Promise(function(r) { 
    setTimeout(r, timeout); 
  }); 
} 
 
function f1() { 
  console.log(1); 
} 
 
function f2() { 
  console.log(2); 
} 
 
function f3() { 
  console.log(3); 
} 
delay(3000).then(f1) 
  .then(function(){return delay(2000);}) 
  .then(f2) 
  .then(function(){ return delay(1000);}) 
  .then(f3) 
  .then(function() { 
    console.log('all finish'); 
  });

Или даже так

function delay(timeout) { 
  return new Promise(function(r) { 
    setTimeout(r, timeout); 
  }); 
} 
 
function f1() { 
  console.log(1); 
  return 2000 
} 
 
function f2() { 
  console.log(2); 
  return 1000; 
} 
 
function f3() { 
  console.log(3); 
} 
delay(3000).then(f1) 
  .then(delay) 
  .then(f2) 
  .then(delay) 
  .then(f3) 
  .then(function() { 
    console.log('all finish'); 
  });

Если таких функций много, то можно собрать из них массив и с помощью функции reduce собрать один большой Promise

function delay(timeout) { 
  return new Promise(function(r) { 
    setTimeout(r, timeout); 
  }); 
} 
 
function f1() { 
  console.log(1); 
  return 2000 
} 
 
function f2() { 
  console.log(2); 
  return 1000; 
} 
 
function f3() { 
  console.log(3); 
} 
 
[f1, f2, f3].reduce(function(promise, func) { 
    return promise.then(func).then(delay); 
  }, delay(3000)) 
  .then(function() { 
    console.log('all finish') 
  });

Answer 3

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

function f1(){ setTimeout( function(){ console.log(1); next(); }, 30); }
function f2(){ setTimeout( function(){ console.log(2); next(); }, 20); }
function f3(){ setTimeout( function(){ console.log(3); next(); }, 10); }
var queue = [f1, f2, f3]
    ,i=0
    ,next = function(){ queue[i]  &&  queue[i++]();}
;
next(); // 1 2 3
READ ALSO
Помощь с подключением js файлом на ajax сайте

Помощь с подключением js файлом на ajax сайте

И так уважаемые эксперты, есть сайт ajax, но котором есть 10 одинаковых страниц, но с разным выводом инфы из бд, и каждый раз требуется выполнять...

110
Как сделать кастомные типы TypeScript (Number, Array, Object, Boolean)?

Как сделать кастомные типы TypeScript (Number, Array, Object, Boolean)?

Можете помочь, пожалуйстаПерехожу с JS На TS, и сразу встал в ступор

102
Не работает код javascript, в консоли ошибок нету

Не работает код javascript, в консоли ошибок нету

начинаю изучать javascript, пишу скрипт, с помощью которого меняется цвет текста по клику мышиПочему-то не работает данный код, консоль собственно...

116
Скролл блока к верху страницы при попадании в видимость экрана

Скролл блока к верху страницы при попадании в видимость экрана

Такая задача: у меня есть блок и при попадании в видимость экрана на него вешается классview

96