Как синхронизировать последовательность обещаний?
У меня есть массив объектов обещаний, которые должны быть разрешены в той же последовательности, в которой они перечислены в массиве, т.е. мы не можем пытаться разрешить элемент, пока предыдущий не был разрешен (как метод 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 как последовательность методов. Метод может выполнять итерацию последовательности динамической длины и создавать обещания в соответствии с требованиями бизнес-логики вашего приложения.
Позже я превратил ее в общую библиотеку для всех желающих.
6 ответов
Вот несколько простых примеров того, как вы последовательно проходите через массив, выполняя каждую асинхронную операцию последовательно (один за другим).
Предположим, у вас есть массив элементов:
var arr = [...];
И вы хотите выполнить определенную асинхронную операцию для каждого элемента в массиве, поочередно, так, чтобы следующая операция не начиналась до тех пор, пока не завершится предыдущая.
Предположим, у вас есть функция возврата обещания для обработки одного из элементов в массиве:
Ручная итерация
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;
return new Promise(function(resolve, reject) {
function next() {
if (index < array.length) {
fn(array[index++]).then(next, reject);
} else {
resolve();
}
}
next();
}
}
processArray(arr, processItem).then(function() {
// all done here
}, function(reason) {
// rejection happened
});
Примечание: это остановит цепочку при первом отклонении и передаст эту причину обратно возвращенному обещанию 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 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/
Некоторые комментарии об этой реализации:
Promise.resolve(factory(idx))
по существу бросает результатfactory(idx)
к обещанию. Если это было просто значение, то оно становится разрешенным обещанием с этим возвращаемым значением в качестве значения разрешения. Если это уже было обещание, то оно просто цепляется за это обещание. Таким образом, он заменяет весь ваш код проверки типа на возвращаемое значениеfactory()
функция.Заводская функция сигнализирует, что это сделано, возвращая либо
null
или обещание, чья решаемая стоимость заканчиваетсяnull
, Приведенный выше преобразователь отображает эти два условия в один и тот же результирующий код.Заводская функция автоматически перехватывает исключения и превращает их в отказы, которые затем автоматически обрабатываются
sequence()
функция. Это одно из существенных преимуществ разрешения обещаний выполнять большую часть обработки ошибок, если вы просто хотите прервать обработку и вернуть ошибку при первом исключении или отклонении.Функция фабрики в этой реализации может возвращать обещание или статическое значение (для синхронной операции), и она будет работать нормально (согласно вашему запросу на разработку).
Я протестировал его с выданным исключением в обратном вызове обещания в фабричной функции, и оно действительно просто отклоняет и передает это исключение, чтобы отклонить обещание последовательности с исключением в качестве причины.
При этом используется тот же метод, что и вы (специально пытаясь придерживаться общей архитектуры) для объединения нескольких вызовов в
loop()
,
Обещания представляют собой ценности операций, а не сами операции. Операции уже начаты, поэтому вы не можете заставить их ждать друг друга.
Вместо этого вы можете синхронизировать функции, которые возвращают обещания, вызывая их по порядку (например, через цикл с цепочкой обещаний) или используя .each
метод в синей птице.
Вы не можете просто запустить асинхронные операции X, а затем хотите, чтобы они были разрешены в порядке.
Правильный способ сделать что-то вроде этого - запустить новую асинхронную операцию только после того, как была решена предыдущая:
doSomethingAsync().then(function(){
doSomethingAsync2().then(function(){
doSomethingAsync3();
.......
});
});
редактировать
Похоже, вы хотите дождаться всех обещаний, а затем вызвать их обратные вызовы в определенном порядке. Что-то вроде этого:
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);
}
});
Проблемы, которые могут возникнуть при этом, - когда одно или несколько обещаний не выполняются.
Я предполагаю два подхода к решению этого вопроса:
- Создайте несколько обещаний и используйте функцию 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'] });
- Вместо этого используйте функцию 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') })()
Хотя это довольно плотное решение, вот еще одно решение, которое будет повторять функцию, возвращающую обещание, по массиву значений и разрешаться с помощью массива результатов:
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, но немного точнее.
На мой взгляд, вы должны использовать цикл for (да, единственный раз, когда я бы рекомендовал цикл 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 в полной мере!!!!
Также в зависимости от ваших настроек или когда вы читаете это, вам может понадобиться добавить plugin-proposal-async-generator-functions
polyfill или вы можете увидеть следующую ошибку
@babel/plugin-proposal-async-generator-functions (https://git.io/vb4yp) to the 'plugins' section of your Babel config to enable transformation.