JavaScript ES6 обещает цикл
for (let i = 0; i < 10; i++) {
const promise = new Promise((resolve, reject) => {
const timeout = Math.random() * 1000;
setTimeout(() => {
console.log(i);
}, timeout);
});
// TODO: Chain this promise to the previous one (maybe without having it running?)
}
Вышеприведенный случай дает следующий случайный вывод:
6
9
4
8
5
1
7
2
3
0
Задача проста: убедитесь, что каждое обещание выполняется только после другого (.then()
).
По какой-то причине я не смог найти способ сделать это.
Я попробовал функции генератора (yield
), попробовал простые функции, которые возвращают обещание, но в конце дня всегда сводится к одной и той же проблеме: Цикл является синхронным.
С async Я бы просто использовал async.series()
.
Как вы его решаете?
Ответы
Ответ 1
Как вы уже намекали в своем вопросе, ваш код создает все обещания синхронно. Вместо этого они должны быть созданы только в момент разрешения предыдущего.
Во-вторых, каждое обещание, которое создается с new Promise
должно быть разрешено с помощью вызова resolve
(или reject
). Это должно быть сделано, когда таймер истекает. Это вызовет любого then
обратный вызов вы бы на этом обещании. И такой then
обратный вызов (или await
) является необходимостью для реализации цепи.
С этими ингредиентами есть несколько способов выполнить эту асинхронную цепочку:
-
С циклом for
который начинается с немедленного разрешения обещания
-
С Array#reduce
который начинается с немедленного разрешения обещания
-
С функцией, которая передает себя как обратный вызов разрешения
-
С ECMAScript2017 async
/await
синтаксис
-
С предложенным ECMAScript2020 for await...of
синтаксиса
Смотрите фрагмент и комментарии для каждого из этих вариантов ниже.
1. С for
Вы можете использовать цикл for
, но вы должны убедиться, что он не выполняет new Promise
синхронно. Вместо этого вы создаете первоначальное обещание с немедленным разрешением, а затем цепочка новых обещаний по мере того, как разрешаются предыдущие:
for (let i = 0, p = Promise.resolve(); i < 10; i++) {
p = p.then(_ => new Promise(resolve =>
setTimeout(function () {
console.log(i);
resolve();
}, Math.random() * 1000)
));
}
Ответ 2
Вы можете использовать async/await
для этого. Я бы объяснил больше, но там ничего нет. Это обычный регулярный цикл for
, но я добавил ключевое слово await
до создания вашего обещания
Что мне нравится в этом, ваше обещание может разрешить нормальное значение вместо того, чтобы иметь побочный эффект, такой как ваш код (или другие ответы здесь). Это дает вам силы, как в "Легенде о Зельде": "Ссылка на прошлое", где вы можете воздействовать на вещи как в свете света, так и в Темном мире, т.е. Вы можете легко работать с данными до/после того, как данные Обещанного доступны, прибегать к глубоко вложенным функциям, другим громоздким структурам управления или глупому IIFE.
// where DarkWorld is in the scary, unknown future
// where LightWorld is the world we saved from Ganondorf
LightWorld ... await DarkWorld
Итак, вот что это будет выглядеть...
const someProcedure = async n =>
{
for (let i = 0; i < n; i++) {
const t = Math.random() * 1000
const x = await new Promise(r => setTimeout(r, t, i))
console.log (i, x)
}
return 'done'
}
someProcedure(10).then(x => console.log(x)) // => Promise
// 0 0
// 1 1
// 2 2
// 3 3
// 4 4
// 5 5
// 6 6
// 7 7
// 8 8
// 9 9
// done
Ответ 3
Основываясь на превосходном ответе trincot, я написал многократно используемую функцию, которая принимает обработчик для запуска каждого элемента в массиве. Сама функция возвращает обещание, которое позволяет вам подождать, пока цикл не завершится, и передаваемая вами функция-обработчик также может вернуть обещание.
цикл (элементы, обработчик): Обещание
Мне потребовалось некоторое время, чтобы понять это правильно, но я верю, что следующий код будет пригоден для использования во многих ситуациях с циклами обещаний.
Скопируйте и вставьте готовый код:
// SEE https://stackoverflow.com/a/46295049/286685
const loop = (arr, fn, busy, err, i=0) => {
const body = (ok,er) => {
try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
catch(e) {er(e)}
}
const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
const run = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
return busy ? run(busy,err) : new Promise(run)
}
использование
Чтобы использовать его, вызовите его с массивом для цикла в качестве первого аргумента и функцией-обработчиком в качестве второго. Не передавайте параметры для третьего, четвертого и пятого аргументов, они используются внутренне.
const loop = (arr, fn, busy, err, i=0) => {
const body = (ok,er) => {
try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
catch(e) {er(e)}
}
const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
const run = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
return busy ? run(busy,err) : new Promise(run)
}
const items = ['one', 'two', 'three']
loop(items, item => {
console.info(item)
})
.then(() => console.info('Done!'))
Ответ 4
Если вы ограничены ES6, лучшим вариантом будет Promise all. Promise.all(array)
также возвращает массив обещаний после успешного выполнения всех обещаний в аргументе array
. Предположим, если вы хотите обновить много записей о студентах в базе данных, следующий код демонстрирует концепцию Promise.all в таком case-
let promises = [];
students.map((student, index) => {
student.rollNo = index + 1;
student.city = 'City Name';
//Update whatever information on student you want
promises.push(student.save());
//where save() is a function used to save data in mongoDB
});
Promise.all(promises).then(() => {
//All the save queries will be executed when .then is executed
//You can do further operations here after as all update operations are completed now
});
Карта является просто примером метода для цикла. Вы также можете использовать for
цикла forin
или forEach
. Так что концепция довольно проста, запустите цикл, в котором вы хотите выполнять массовые асинхронные операции. Переместите каждый такой оператор асинхронной операции в массив, объявленный вне области действия этого цикла. После завершения цикла выполните инструкцию Promise all с подготовленным массивом таких запросов/обещаний в качестве аргумента.
Основная концепция заключается в том, что цикл javascript является синхронным, тогда как вызов базы данных является асинхронным, и мы используем метод push в цикле, который также является синхронизированным. Таким образом, проблема асинхронного поведения не возникает внутри цикла.
Ответ 5
здесь мои 2 цента стоит:
- resuable function
forpromise()
- эмулирует классический цикл
- позволяет выполнить ранний выход на основе внутренней логики, возвращая значение
- может собирать массив результатов, переданных в разрешение /next/collect
- defaults to start = 0, increment = 1
- Исключения, заброшенные внутри цикла, пойманы и переданы в .catch()
function forpromise(lo, hi, st, res, fn) {
if (typeof res === 'function') {
fn = res;
res = undefined;
}
if (typeof hi === 'function') {
fn = hi;
hi = lo;
lo = 0;
st = 1;
}
if (typeof st === 'function') {
fn = st;
st = 1;
}
return new Promise(function(resolve, reject) {
(function loop(i) {
if (i >= hi) return resolve(res);
const promise = new Promise(function(nxt, brk) {
try {
fn(i, nxt, brk);
} catch (ouch) {
return reject(ouch);
}
});
promise.
catch (function(brkres) {
hi = lo - st;
resolve(brkres)
}).then(function(el) {
if (res) res.push(el);
loop(i + st)
});
})(lo);
});
}
//no result returned, just loop from 0 thru 9
forpromise(0, 10, function(i, next) {
console.log("iterating:", i);
next();
}).then(function() {
console.log("test result 1", arguments);
//shortform:no result returned, just loop from 0 thru 4
forpromise(5, function(i, next) {
console.log("counting:", i);
next();
}).then(function() {
console.log("test result 2", arguments);
//collect result array, even numbers only
forpromise(0, 10, 2, [], function(i, collect) {
console.log("adding item:", i);
collect("result-" + i);
}).then(function() {
console.log("test result 3", arguments);
//collect results, even numbers, break loop early with different result
forpromise(0, 10, 2, [], function(i, collect, break_) {
console.log("adding item:", i);
if (i === 8) return break_("ending early");
collect("result-" + i);
}).then(function() {
console.log("test result 4", arguments);
// collect results, but break loop on exception thrown, which we catch
forpromise(0, 10, 2, [], function(i, collect, break_) {
console.log("adding item:", i);
if (i === 4) throw new Error("failure inside loop");
collect("result-" + i);
}).then(function() {
console.log("test result 5", arguments);
}).
catch (function(err) {
console.log("caught in test 5:[Error ", err.message, "]");
});
});
});
});
});