Подтвердить что ты не робот

Как синхронизировать последовательность promises?

У меня есть массив объектов обещаний, которые должны быть разрешены в той же последовательности, в которой они перечислены в массиве, т.е. мы не можем пытаться разрешить элемент, пока не будет разрешен предыдущий (как метод Promise.all([...]) делает).

И если один элемент отклонен, мне нужно сразу отклонить цепочку, не пытаясь разрешить следующий элемент.

Как я могу реализовать это, или существует существующая реализация для такого шаблона sequence?

function sequence(arr) {
    return new Promise(function (resolve, reject) {
        // try resolving all elements in 'arr',
        // but strictly one after another;
    });
}

РЕДАКТИРОВАТЬ

Первоначальные ответы предполагают, что мы можем только sequence результаты таких элементов массива, а не их выполнение, потому что это предопределено в таком примере.

Но тогда как создать массив обещаний таким образом, чтобы избежать раннего выполнения?

Вот модифицированный пример:

function sequence(nextPromise) {
    // while nextPromise() creates and returns another promise,
    // continue resolving it;
}

Я бы не хотел делать это отдельным вопросом, потому что считаю, что это часть той же проблемы.

РЕШЕНИЕ

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

Позже я превратил ее в общую библиотеку для всех желающих.

4b9b3361

Ответ 1

Вот несколько простых примеров того, как вы последовательно проходите через массив, выполняя каждую асинхронную операцию последовательно (один за другим).

Предположим, у вас есть массив элементов:

var arr = [...];

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

И, допустим, у вас есть функция возврата обещания для обработки одного из элементов в массиве fn(item):

Ручная итерация

function processItem(item) {
    // do async operation and process the result
    // return a promise
}

Затем вы можете сделать что-то вроде этого:

function processArray(array, fn) {
    var index = 0;

    function next() {
        if (index < array.length) {
            fn(array[index++]).then(next);
        }
    }
    next();
}

processArray(arr, processItem);

Ручная итерация, возвращающая обещание

Если вы хотите вернуть обещание из processArray() чтобы знать, когда оно выполнено, вы можете добавить к нему следующее:

function processArray(array, fn) {
    var index = 0;

    function next() {
        if (index < array.length) {
            return fn(array[index++]).then(function(value) {
                // apply some logic to value
                // you have three options here:
                // 1) Call next() to continue processing the result of the array
                // 2) throw err to stop processing and result in a rejected promise being returned
                // 3) return value to stop processing and result in a resolved promise being returned
                return next();
            });
        }
    } else {
        // return whatever you want to return when all processing is done
        // this returne value will be the ersolved value of the returned promise.
        return "all done";
    }
}

processArray(arr, processItem).then(function(result) {
    // all done here
    console.log(result);
}, function(err) {
    // rejection happened
    console.log(err);
});

Примечание: это остановит цепочку при первом отклонении и передаст эту причину обратно возвращенному обещанию processArray.

Итерация с .reduce()

Если вы хотите выполнить больше работы с обещаниями, вы можете связать все обещания:

function processArray(array, fn) {
   return array.reduce(function(p, item) {
       return p.then(function() {
          return fn(item);
       });
   }, Promise.resolve());
}

processArray(arr, processItem).then(function(result) {
    // all done here
}, function(reason) {
    // rejection happened
});

Примечание: это остановит цепочку при первом отклонении и передаст эту причину обратно обещанию, возвращенному из processArray().

В случае успеха обещание, возвращаемое из processArray() будет разрешено с последним разрешенным значением вашего обратного вызова fn. Если вы хотите накапливать список результатов и обрабатывать их, вы можете собрать результаты в массиве замыканий из fn и продолжать возвращать этот массив каждый раз, чтобы окончательное решение было массивом результатов.

Итерация с .reduce(), которая разрешается с массивом

И, поскольку теперь кажется очевидным, что вы хотите, чтобы конечный результат обещания представлял собой массив данных (по порядку), вот ревизия предыдущего решения, которое производит это:

function processArray(array, fn) {
   var results = [];
   return array.reduce(function(p, item) {
       return p.then(function() {
           return fn(item).then(function(data) {
               results.push(data);
               return results;
           });
       });
   }, Promise.resolve());
}

processArray(arr, processItem).then(function(result) {
    // all done here
    // array of data here in result
}, function(reason) {
    // rejection happened
});

Рабочая демонстрация: http://jsfiddle.net/jfriend00/h3zaw8u8/

И рабочая демонстрация, которая показывает отказ: http://jsfiddle.net/jfriend00/p0ffbpoc/

Итерация с .reduce(), которая разрешается с массивом с задержкой

И, если вы хотите вставить небольшую задержку между операциями:

function delay(t, v) {
    return new Promise(function(resolve) {
        setTimeout(resolve.bind(null, v), t);
    });
}

function processArrayWithDelay(array, t, fn) {
   var results = [];
   return array.reduce(function(p, item) {
       return p.then(function() {
           return fn(item).then(function(data) {
               results.push(data);
               return delay(t, results);
           });
       });
   }, Promise.resolve());
}

processArray(arr, 200, processItem).then(function(result) {
    // all done here
    // array of data here in result
}, function(reason) {
    // rejection happened
});

Итерация с библиотекой обещаний Bluebird

Библиотека обещаний Bluebird имеет много встроенных функций управления параллелизмом. Например, для последовательной итерации по массиву вы можете использовать Promise.mapSeries().

Promise.mapSeries(arr, function(item) {
    // process each individual item here, return a promise
    return processItem(item);
}).then(function(results) {
    // process final results here
}).catch(function(err) {
    // process array here
});

Или вставить задержку между итерациями:

Promise.mapSeries(arr, function(item) {
    // process each individual item here, return a promise
    return processItem(item).delay(100);
}).then(function(results) {
    // process final results here
}).catch(function(err) {
    // process array here
});

Использование ES7 async/await

Если вы кодируете в среде, которая поддерживает async/await, вы также можете просто использовать регулярный цикл for а затем await обещание в цикле, и это заставит цикл for приостановиться, пока обещание не будет разрешено, прежде чем продолжить. Это эффективно упорядочит ваши асинхронные операции, поэтому следующая не начнется, пока не будет выполнена предыдущая.

async function processArray(array, fn) {
    let results = [];
    for (let i = 0; i < array.length; i++) {
        let r = await fn(array[i]);
        results.push(r);
    }
    return results;    // will be resolved value of promise
}

// sample usage
processArray(arr, processItem).then(function(result) {
    // all done here
    // array of data here in result
}, function(reason) {
    // rejection happened
});

К вашему сведению, моя processArray() здесь очень похожа на Promise.map() в библиотеке обещаний Bluebird, которая принимает массив и функцию создания обещаний и возвращает обещание, которое разрешается с массивом разрешенных результатов.


@vitaly-t - Вот несколько более подробных комментариев о вашем подходе. Добро пожаловать в любой код, который вам кажется лучшим. Когда я впервые начал использовать обещания, я имел тенденцию использовать обещания только для самых простых вещей, которые они делали, и сам писал большую логику, когда более продвинутое использование обещаний могло бы сделать для меня гораздо больше. Вы используете только то, что вам удобно, и даже больше, вы предпочитаете видеть свой собственный код, который вы глубоко знаете. Это, вероятно, человеческая природа.

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

Вот некоторые конкретные отзывы о вашем подходе:

Вы создаете обещания в семи местах

В отличие от стилей, в моем коде есть только два места, где я явно создаю новое обещание - один раз в фабричной функции и один раз для инициализации цикла .reduce(). В другом месте я просто опираюсь на обещания, которые уже были созданы цепочкой или возвращением значений внутри них, или просто возвращением их напрямую. В вашем коде есть семь уникальных мест, где вы создаете обещание. Теперь хорошее кодирование - это не соревнование, чтобы увидеть, как мало мест, где вы можете создать обещание, но это может указать на разницу в использовании обещаний, которые уже созданы, в сравнении с условиями тестирования и созданием новых обещаний.

Бросок безопасности очень полезная функция

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

Много Promise.resolve() или Promise.reject(), вероятно, возможность для упрощения

Если вы видите код с множеством Promise.resolve() или Promise.reject(), то, вероятно, есть возможности лучше использовать существующие обещания, а не создавать все эти новые обещания.

Приведение к обещанию

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

Договор на возврат обещания

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

Функция фабрики может быть написана для создания только одного обещания

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

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

function factory(idx) {
    // create the promise this way gives you automatic throw-safety
    return new Promise(function(resolve, reject) {
        switch (idx) {
            case 0:
                resolve("one");
                break;
            case 1:
                resolve("two");
                break;
            case 2:
                resolve("three");
                break;
            default:
                resolve(null);
                break;
        }
    });
}

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

function factory(idx) {
    // create the promise this way gives you automatic throw-safety
    return new Promise(function(resolve, reject) {
        switch (idx) {
            case 0:
                resolve($.ajax(...));
            case 1:
                resole($.ajax(...));
            case 2:
                resolve("two");
                break;
            default:
                resolve(null);
                break;
        }
    });
}

Использовать обработчик отклонения для return promise.reject(reason) не требуется. return promise.reject(reason) не нужна

Когда у вас есть это тело кода:

    return obj.then(function (data) {
        result.push(data);
        return loop(++idx, result);
    }, function (reason) {
        return promise.reject(reason);
    });

Обработчик отклонения не добавляет никакого значения. Вместо этого вы можете просто сделать это:

    return obj.then(function (data) {
        result.push(data);
        return loop(++idx, result);
    });

Вы уже возвращаете результат obj.then(). Если либо obj отклоняет, либо если что-либо, связанное с obj или возвращаемое из него, обработчик .then() отклоняет, obj будет отклонен. Таким образом, вам не нужно создавать новое обещание с отклонением. Более простой код без обработчика отклонения делает то же самое с меньшим количеством кода.


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

function factory(idx) {
    // create the promise this way gives you automatic throw-safety
    return new Promise(function(resolve, reject) {
        switch (idx) {
            case 0:
                resolve("zero");
                break;
            case 1:
                resolve("one");
                break;
            case 2:
                resolve("two");
                break;
            default:
                // stop further processing
                resolve(null);
                break;
        }
    });
}


// Sequentially resolves dynamic promises returned by a factory;
function sequence(factory) {
    function loop(idx, result) {
        return Promise.resolve(factory(idx)).then(function(val) {
            // if resolved value is not null, then store result and keep going
            if (val !== null) {
                result.push(val);
                // return promise from next call to loop() which will automatically chain
                return loop(++idx, result);
            } else {
                // if we got null, then we're done so return results
                return result;
            }
        });
    }
    return loop(0, []);
}

sequence(factory).then(function(results) {
    log("results: ", results);
}, function(reason) {
    log("rejected: ", reason);
});

Рабочая демонстрация: http://jsfiddle.net/jfriend00/h3zaw8u8/

Некоторые комментарии об этой реализации:

  1. Promise.resolve(factory(idx)) существу приводит результат factory(idx) к обещанию. Если это было просто значение, то оно становится разрешенным обещанием с этим возвращаемым значением в качестве значения разрешения. Если это уже было обещание, то оно просто цепляется за это обещание. Таким образом, он заменяет весь ваш код проверки типа на возвращаемое значение функции factory().

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

  3. Заводская функция автоматически перехватывает исключения и превращает их в отклонения, которые затем автоматически обрабатываются функцией sequence(). Это одно существенное преимущество, которое позволяет обещаниям выполнять большую часть обработки ошибок, если вы просто хотите прервать обработку и вернуть ошибку при первом исключении или отклонении.

  4. Функция фабрики в этой реализации может возвращать обещание или статическое значение (для синхронной операции), и она будет работать нормально (согласно вашему запросу на разработку).

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

  6. При этом используется тот же метод, что и вы (специально пытаясь придерживаться общей архитектуры) для объединения нескольких вызовов loop().

Ответ 2

Promises представляют значения операций, а не сами операции. Операции уже запущены, поэтому вы не можете заставить их ждать друг друга.

Вместо этого вы можете синхронизировать функции, которые возвращают promises, вызывающие их по порядку (например, через цикл с цепочкой обещаний) или используя метод .each в bluebird.

Ответ 3

Вы не можете просто запускать X-операции async, а затем хотите, чтобы они были разрешены в порядке.

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

doSomethingAsync().then(function(){
   doSomethingAsync2().then(function(){
       doSomethingAsync3();
       .......
   });
});

Изменить
Кажется, что вы хотите подождать все promises, а затем вызвать их обратные вызовы в определенном порядке. Что-то вроде этого:

var callbackArr = [];
var promiseArr = [];
promiseArr.push(doSomethingAsync());
callbackArr.push(doSomethingAsyncCallback);
promiseArr.push(doSomethingAsync1());
callbackArr.push(doSomethingAsync1Callback);
.........
promiseArr.push(doSomethingAsyncN());
callbackArr.push(doSomethingAsyncNCallback);

а затем:

$.when(promiseArr).done(function(promise){
    while(callbackArr.length > 0)
    {
       callbackArr.pop()(promise);
    }
});

Проблемы, которые могут возникнуть в связи с этим, - это отказ одного или более promises.

Ответ 4

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

function processArray(arr, fn) {
    return arr.reduce(
        (p, v) => p.then((a) => fn(v).then(r => a.concat([r]))),
        Promise.resolve([])
    );
}

Использование:

const numbers = [0, 4, 20, 100];
const multiplyBy3 = (x) => new Promise(res => res(x * 3));

// Prints [ 0, 12, 60, 300 ]
processArray(numbers, multiplyBy3).then(console.log);

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

Он функционально эквивалентен "Итерации с .reduce(), который разрешает с помощью массива" решение от @jfriend00 ", но немного опрятным.

Ответ 5

Я предполагаю два подхода к решению этого вопроса:

  1. Создайте несколько обещаний и используйте функцию allWithAsync следующим образом:
let allPromiseAsync = (...PromisesList) => {
return new Promise(async resolve => {
    let output = []
    for (let promise of PromisesList) {
        output.push(await promise.then(async resolvedData => await resolvedData))
        if (output.length === PromisesList.length) resolve(output)
    }
}) }
const prm1= Promise.resolve('first');
const prm2= new Promise((resolve, reject) => setTimeout(resolve, 2000, 'second'));
const prm3= Promise.resolve('third');

allPromiseAsync(prm1, prm2, prm3)
    .then(resolvedData => {
        console.log(resolvedData) // ['first', 'second', 'third']
    });
  1. Вместо этого используйте функцию Promise.all:
  (async () => {
  const promise1 = new Promise(resolve => {
    setTimeout(() => { resolve() }, 2500)
  })

  const promise2 = new Promise(resolve => {
    setTimeout(() => { resolve() }, 5000)
  })

  const promise3 = new Promise(resolve => {
    setTimeout(() => { resolve() }, 1000)
  })

  const promises = [promise1, promise2, promise3]

  await Promise.all(promises)

  console.log('This line is shown after 8500ms')
})()

Ответ 6

На мой взгляд, вы должны использовать цикл for (да, единственный раз, когда я бы рекомендовал цикл for). Причина заключается в том, что, когда вы используете для цикла он позволяет await на каждом из итераций вашего цикла, где с помощью reduce, map или forEach с пробегом всех итераций обещания одновременно. Что, судя по звукам, не то, что вы хотите, вы хотите, чтобы каждое обещание подождало, пока предыдущее обещание не будет решено. Таким образом, чтобы сделать это, вы должны сделать что-то вроде следующего.

const ids = [0, 1, 2]
const accounts = ids.map(id => getId(id))
const accountData = async() => {
   for await (const account of accounts) {
       // account will equal the current iteration of the loop
       // and each promise are now waiting on the previous promise to resolve! 
   }
}

// then invoke your function where ever needed
accountData()

И, очевидно, если вы хотите стать действительно экстремальным, вы можете сделать что-то вроде этого:

 const accountData = async(accounts) => {
    for await (const account of accounts) {
       // do something
    }
 }

 accountData([0, 1, 2].map(id => getId(id)))

Это намного более читабельно, чем любой из других примеров, это гораздо меньше кода, уменьшает количество строк, необходимых для этой функциональности, следует более функциональному программированию и использует ES7 в полной мере !!!!

Также в зависимости от ваших настроек или когда вы читаете это, вам может понадобиться добавить polyfill plugin-proposal-async-generator-functions offer plugin-proposal-async-generator-functions или вы можете увидеть следующую ошибку

@babel/plugin-proposal-async-generator-functions (https://git.io/vb4yp) to the 'plugins' section of your Babel config to enable transformation.