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
),尝试了返回promise的简单函数,但在一天结束时它总是归结为同样的问题:循环是同步的
使用async我只需使用async.series()
。
你是如何解决的?
答案 0 :(得分:201)
正如您在问题中已经暗示的那样,您的代码会同步创建所有承诺。相反,它们只应在前一个结算时创建。
其次,需要通过调用new Promise
(或resolve
)来解决使用reject
创建的每个承诺。这应该在计时器到期时完成。这将触发您对该承诺的任何then
回调。这样的then
回调(或await
)是实现链的必要条件。
使用这些成分,有几种方法可以执行此异步链接:
for
循环以立即解决的承诺开头
Array#reduce
以立即解决的承诺
使用将自身作为分辨率回调
使用ECMAScript2017&#39; async
/ await
syntax
建议使用ECMAScript2020 for await...of
syntax
请参阅以下每个选项的摘要和评论。
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)
));
}
&#13;
reduce
这只是对以前策略更具功能性的方法。您创建一个与您要执行的链长度相同的数组,并从一个立即解决的承诺开始:
[...Array(10)].reduce( (p, _, i) =>
p.then(_ => new Promise(resolve =>
setTimeout(function () {
console.log(i);
resolve();
}, Math.random() * 1000)
))
, Promise.resolve() );
&#13;
当您实际拥有一个数据以便在promises中使用时,这可能更有用。
这里我们创建一个函数并立即调用它。它同步创造了第一个承诺。解析后,再次调用该函数:
(function loop(i) {
if (i < 10) new Promise((resolve, reject) => {
setTimeout( () => {
console.log(i);
resolve();
}, Math.random() * 1000);
}).then(loop.bind(null, i+1));
})(0);
&#13;
这将创建一个名为loop
的函数,在代码的最后,您可以看到它会立即被参数0调用。这是计数器,以及 i 参数。如果该计数器仍然低于10,该函数将创建一个新的承诺,否则链接将停止。
对resolve()
的调用将触发then
回调,该回调将再次调用该函数。 loop.bind(null, i+1)
只是_ => loop(i+1)
的另一种说法。
async
/ await
现代JS引擎support this syntax:
(async function loop() {
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
console.log(i);
}
})();
&#13;
它可能看起来很奇怪,因为似乎就像new Promise()
调用是同步执行的,但实际上async
函数会返回执行第一个await
。每当等待的承诺解析时,函数的运行上下文将被恢复,并在await
之后继续,直到遇到下一个,然后一直持续到循环结束。
由于基于超时返回承诺可能是常见的事情,您可以创建一个单独的函数来生成这样的承诺。这称为 promisifying 一个函数,在本例中为setTimeout
。它可以提高代码的可读性:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
(async function loop() {
for (let i = 0; i < 10; i++) {
await delay(Math.random() * 1000);
console.log(i);
}
})();
&#13;
for await...of
最近,for await...of
语法找到了一些JavaScript引擎。虽然在这种情况下它并没有真正减少代码,但它允许将随机区间链的定义与其实际迭代隔离开来:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
async function * randomDelays(count ,max) {
for (let i = 0; i < count; i++) yield delay(Math.random() * max).then(() => i);
}
(async function loop() {
for await (let i of randomDelays(10, 1000)) console.log(i);
})();
&#13;
答案 1 :(得分:10)
您可以使用async/await
。我会解释得更多,但没有什么真正的。它只是一个常规的for
循环,但我在构建您的承诺之前添加了await
关键字
我喜欢这个是你的Promise可以解决一个正常的值,而不是像你的代码(或其他答案)包括副作用。这为你提供了像塞尔达传说:过去的链接这样的力量,在那里你可以影响光世界和黑暗世界中的事物 - 也就是说,你可以轻松地在Promised数据可用之前/之后处理数据,而不必诉诸深层嵌套函数,其他笨拙的控制结构或愚蠢的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
&#13;
了解我们如何处理我们程序中令人烦恼的.then
电话? async
关键字会自动确保返回Promise
,因此我们可以对返回的值进行.then
链接。这为我们取得了巨大的成功:运行n
Promises的序列,然后做一些重要的事情 - 比如显示成功/错误消息。
答案 2 :(得分:3)
基于trincot的优秀答案,我编写了一个可重用的函数,它接受一个处理程序来遍历数组中的每个项目。函数本身返回一个promise,它允许你等到循环结束,你传递的处理函数也可以返回一个promise。
我花了一些时间才能做到正确,但我相信以下代码可用于许多承诺循环的情况。
复制粘贴就绪代码:
// 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!'))
让我们看一下处理函数,嵌套循环和错误处理。
处理程序获得3个参数。当前项目,当前项目的索引和循环的完整数组。如果处理函数需要执行异步工作,它可以返回一个promise,并且循环函数将在开始下一次迭代之前等待promise解析。您可以嵌套循环调用,并且所有工作都按预期工作。
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 tests = [
[],
['one', 'two'],
['A', 'B', 'C']
]
loop(tests, (test, idx, all) => new Promise((testNext, testFailed) => {
console.info('Performing test ' + idx)
return loop(test, (testCase) => {
console.info(testCase)
})
.then(testNext)
.catch(testFailed)
}))
.then(() => console.info('All tests done'))
我看到许多发生异常的许多循环示例。让这个功能做正确的事情是相当棘手的,但据我所知它现在正在运作。确保为任何内部循环添加一个catch处理程序,并在发生时调用reject函数。 E.g:
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 tests = [
[],
['one', 'two'],
['A', 'B', 'C']
]
loop(tests, (test, idx, all) => new Promise((testNext, testFailed) => {
console.info('Performing test ' + idx)
loop(test, (testCase) => {
if (idx == 2) throw new Error()
console.info(testCase)
})
.then(testNext)
.catch(testFailed) // <--- DON'T FORGET!!
}))
.then(() => console.error('Oops, test should have failed'))
.catch(e => console.info('Succesfully caught error: ', e))
.then(() => console.info('All tests done'))
自写这个答案以来,我将上述代码转换为NPM包。
npm install --save for-async
var forAsync = require('for-async'); // Common JS, or
import forAsync from 'for-async';
var arr = ['some', 'cool', 'array'];
forAsync(arr, function(item, idx){
return new Promise(function(resolve){
setTimeout(function(){
console.info(item, idx);
// Logs 3 lines: `some 0`, `cool 1`, `array 2`
resolve(); // <-- signals that this iteration is complete
}, 25); // delay 25 ms to make async
})
})
有关详细信息,请参阅软件包自述文件。
答案 3 :(得分:1)
如果限于ES6,最好的选择是Promise all。在成功执行Promise.all(array)
参数中的所有诺言之后,array
还返回一个诺言数组。
假设,如果您想更新数据库中的许多学生记录,则以下代码演示Promise.all的概念-
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
});
Map只是循环的示例方法。您还可以使用for
或forin
或forEach
循环。因此,此概念非常简单,请启动要执行批量异步操作的循环。将每个此类异步操作语句推送到该循环范围之外声明的数组中。循环完成后,以准备好的查询/承诺数组作为参数执行Promise all语句。
基本概念是javascript循环是同步的,而数据库调用是异步的,我们在循环中使用push方法也是同步的。因此,异步行为的问题不会在循环内发生。
答案 4 :(得分:0)
这是我的2美分价值:
forpromise()
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, "]");
});
});
});
});
});