Последовательный запуск асинхронных функций с промисами

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

var order = [];
var tasks = [
    new Promise(resolve => {
        order.push(1);
        setTimeout(() => {
            order.push(2)
            resolve();
        }, 100);
    }),
    new Promise(resolve => {
        order.push(3);
        setTimeout(() => {
            order.push(4)
            resolve();
        }, 100);
    }),
    new Promise(resolve => {
        order.push(5);
        resolve();
    })
];

tasks.reduce((cur, next) => cur.then(next), Promise.resolve()).then(() => {
    console.log(order); // [ 1, 3, 5 ]
});
setTimeout(() => console.log(order), 200); // [ 1, 3, 5, 2, 4 ]

Я ожидаю, что order будет равно [ 1, 2, 3, 4, 5 ] в функции обратного вызова. Однако я получил эти странные результаты ([ 1, 3, 5 ] в обратном вызове then и [ 1, 3, 5, 2, 4 ] в отложенной функции). Что мне не хватает?


person madox2    schedule 24.03.2016    source источник
comment
Я делаю то же самое в Angular, используя $q.serial отсюда: codeducky.org/q- serial Вы можете избавиться от специфического кода angular и настроить его в соответствии с вашими потребностями. Вы также можете взглянуть на метод waterfall из библиотеки async: github.com/caolan/async   -  person cl3m    schedule 24.03.2016
comment
Функция редукции не ожидает разрешения предыдущего обещания, прежде чем перейти к следующей записи в массиве, поэтому вы запускаете все три нажатия сразу по пути прохождения цикла. setTimeout задерживает разрешение(), но не задерживает действия внутри обещания.   -  person jmcgriz    schedule 24.03.2016
comment
В тот момент, когда вы пишете new Promise(function() {..., выполняется обратный вызов обещания (вы используете стрелочные функции, но то же самое). Если вы хотите запускать их последовательно, а не параллельно, вы не можете создавать такие промисы в массиве, вам нужно придумать что-то другое.   -  person adeneo    schedule 24.03.2016


Ответы (4)


Когда вы пишете что-то вроде

new Promise(resolve => {
    order.push(1);
    setTimeout(() => {
        order.push(2)
        resolve();
    }, 100);
});

она выполняется сразу же, то есть запускается сейчас и разрешается через 0,1 секунды.
Неважно, что вы записываете ее внутри массива, функции по-прежнему выполняются прямо сейчас< /em>, а промисы возвращаются как значения в массиве.

Другими словами, все три вызова промисов выполняются параллельно, все они выполняются сразу, с разницей всего в миллисекунды, и разрешаются в заданное время внутреннего таймера, начиная с сейчас!

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

var tasks = [
    _ => new Promise(resolve => {
            order.push(1);
            setTimeout(() => {
                order.push(2)
                resolve();
            }, 100);
    }),
    _ => new Promise(resolve => {
            order.push(3);
            setTimeout(() => {
                order.push(4)
                resolve();
            }, 100);
    }),
    _ => new Promise(resolve => {
            order.push(5);
            resolve();
    }),
];

(подчеркивание является допустимым сокращением ES2015 для анонимной стрелочной функции)

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

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

(function iterate(i) {
    tasks[i]().then(() => {          // when done
        if (tasks[++i]) iterate(i);  // call the next one
    });
})(0);

FIDDLE


Редактировать:

Вы также можете Array.reduce так, как вы это уже делаете, теперь, когда у вас есть функции, которые возвращают промисы

tasks.reduce((cur, next) => cur.then(next), Promise.resolve()).then(() => {
    // all done, chain is complete !
});
person adeneo    schedule 24.03.2016
comment
Большое спасибо! Вы точно объяснили, что я сделал не так. Видите ли вы какую-либо выгоду от использования этого рекурсивного решения по сравнению с сокращением? - person madox2; 24.03.2016
comment
Просто кажется, что нечего уменьшать? У вас есть массив обещаний, вы на самом деле не хотите уменьшать его до одного, вы просто хотите вызывать их последовательно, а значения находятся в отдельном массиве order, поэтому вы, похоже, не хотите уменьшать это либо ? - person adeneo; 24.03.2016
comment
Решение с сокращением работает, когда я меняю задачи в массиве на функции, которые возвращают промисы. С уменьшенной версией я могу использовать then для добавления обратного вызова, который выполняется после завершения всех задач. - person madox2; 24.03.2016
comment
Да, похоже, вы правы, reduce работает, и это ловкий трюк. У вас есть tasks.reduce((cur, next) => cur.then(next), Promise.resolve()), где разрешенное обещание передается в качестве начального значения, а затем оно продолжает вызывать функцию next после завершения функции cur, а сокращенное последнее обещание разрешается, когда все последовательные вызовы выполнены. Для этого часто используются рекурсивные вызовы функций, и вы можете использовать Promise.all для проверки того, что все обещания разрешены, но это будет то же самое, что и reduce, которое у вас уже есть, так что придерживайтесь этого. - person adeneo; 24.03.2016

Это безобещающий и немного странный способ ведения дел, но, похоже, он работает:

"use strict";
var order = [];
var i = 0;
function next(){
    if(tasks[++i]) tasks[i]()
}
var tasks = [
    function() {
        order.push(1);
        setTimeout(() => {
            order.push(2)
            next()
        }, 100);
    },
    function() {
        order.push(3);
        setTimeout(() => {
            order.push(4)
            next();
        }, 100);
   },
   function() {
        order.push(5);
        next()
        console.log(order)
    }
];

tasks[0]()
person jmcgriz    schedule 24.03.2016

Вы упускаете из виду тот факт, что когда вы используете setTimeout, обратные вызовы (которые отправляют 2, 4 и регистрируют order) будут выполняться на следующей итерации цикла событий, то есть на следующем тике '. В то время как все остальные функции (конструкторы Promise и обратный вызов reduce) выполняются немедленно, то есть в текущем "тике".

var order = [];
var tasks = [
    new Promise(resolve => {
        order.push(1); // 1. callback executes immediately pushing 1 into order
        setTimeout(() => { // 2. setTimeout executes, pushing the callback into the event queue after 100ms
            order.push(2) // 8. callback executes, pushing 2 into order
            resolve();
        }, 100);
    }),
    new Promise(resolve => {
        order.push(3); // 3. callback executes immediately pushing 3 into order
        setTimeout(() => { // 4. setTimeout executes, pushing the callback into the event queue after 100ms
            order.push(4) // 9. callback executes, pushing 4 into order
            resolve();
        }, 100);
    }),
    new Promise(resolve => {
        order.push(5); // 5. callback executes immediately pushing 5 into order 
        resolve();
    })
];

console.log(order); // [ 1, 3, 5 ]    

// 6. reduce executes immediately, executes Promise.resolve which logs order and then loops through order and executes the callback everytime
tasks.reduce((cur, next) => cur.then(next), Promise.resolve()).then(() => {
    console.log(order); // [ 1, 3, 5 ]
});

setTimeout(() => {
    console.log(order); // 10. callback executes and logs order
}, 200); // 7. setTimeout executes, pushing the callback into the event queue after 200ms

Только после выполнения шагов с 1 по 7 все обратные вызовы, которые были помещены в очередь событий (setTimeout), будут выполнены, т. е. помещены в стек вызовов, очищая очередь событий и, в конечном итоге, очищая стек вызовов. после выполнения указанных обратных вызовов (шаги 8, 9 и 10).

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

person danillouz    schedule 24.03.2016

Каждая асинхронная функция имеет синхронную часть, настройку, ведущую к (синхронному) возврату промиса.

person jib    schedule 24.03.2016