JavaScript ES6承诺循环[重复]
Posted
技术标签:
【中文标题】JavaScript ES6承诺循环[重复]【英文标题】:JavaScript ES6 promise for loop 【发布时间】:2017-03-12 18:06:01 【问题描述】: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
任务很简单:确保每个 Promise 仅在另一个 Promise 之后运行 (.then()
)。
由于某种原因,我找不到办法。
我尝试了生成器函数 (yield
),尝试了返回承诺的简单函数,但归根结底,它总是归结为同一个问题:循环是同步的。
对于async,我只需使用async.series()
。
你是怎么解决的?
【问题讨论】:
相关:Creating a (ES6) promise without starting to resolve it 【参考方案1】:正如您在问题中已经暗示的那样,您的代码会同步创建所有承诺。相反,它们应该只在前一个解决时创建。
其次,使用new Promise
创建的每个promise 都需要通过调用resolve
(或reject
)来解决。这应该在计时器到期时完成。这将触发您对该承诺的任何then
回调。而这样的then
回调(或await
)是实现链的必要条件。
有了这些成分,有几种方法可以执行这种异步链接:
for
循环以立即解决的承诺开始
Array#reduce
以立即解决的承诺开头
使用将自身作为解析回调传递的函数
使用 ECMAScript2017 的 async
/ await
syntax
使用 ECMAScript2020 的 for await...of
syntax
但让我先介绍一个非常有用的通用函数。
承诺setTimeout
使用setTimeout
很好,但我们实际上需要一个在计时器到期时解析的promise。所以让我们创建这样一个函数:这被称为promisifying一个函数,在这种情况下我们将promisifysetTimeout
。它将提高代码的可读性,并且可以用于上述所有选项:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
查看以下每个选项的 sn-p 和 cmets。
1。与for
您可以使用for
循环,但您必须确保它不会同步创建所有承诺。相反,您创建一个初始的立即解决承诺,然后在之前的承诺解决时链接新的承诺:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
for (let i = 0, p = Promise.resolve(); i < 10; i++)
p = p.then(() => delay(Math.random() * 1000))
.then(() => console.log(i));
所以这段代码创建了一长串then
调用。变量p
仅用于不丢失该链的跟踪,并允许循环的下一次迭代在同一链上继续。回调将在同步循环完成后开始执行。
重要的是then
-callback返回delay()
创建的承诺:这将确保异步链接。
2。与reduce
这只是对先前策略的一种更实用的方法。您创建一个与要执行的链长度相同的数组,并从立即解决的承诺开始:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
[...Array(10)].reduce( (p, _, i) =>
p.then(() => delay(Math.random() * 1000))
.then(() => console.log(i))
, Promise.resolve() );
当您实际上拥有一个包含要在 Promise 中使用的数据的数组时,这可能会更有用。
3。使用一个函数将自身作为解析回调
这里我们创建一个函数并立即调用它。它同步创建第一个承诺。当它解析时,再次调用该函数:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
(function loop(i)
if (i >= 10) return; // all done
delay(Math.random() * 1000).then(() =>
console.log(i);
loop(i+1);
);
)(0);
这将创建一个名为loop
的函数,在代码的最后你可以看到它被立即调用,参数为 0。这是计数器和 i 参数。如果该计数器仍低于 10,该函数将创建一个新的 Promise,否则链接将停止。
当delay()
解析后,会触发then
回调,再次调用该函数。
4。与async
/await
现代 JS 引擎support this syntax:
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);
)();
这可能看起来很奇怪,因为它似乎承诺是同步创建的,但实际上async
函数在执行第一个await
时返回。每次等待的 Promise 解决时,函数的运行上下文都会恢复,并在 await
之后继续,直到遇到下一个,然后继续循环,直到循环结束。
5。与for await...of
借助 EcmaScript 2020,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);
)();
【讨论】:
我只是想知道这家伙是怎么想出这么多解决方案的。 最后一种方法是完美的,我们只需要在外部函数中添加async
,在循环内添加await
和Promises
@trincot 非常感谢你!这个答案极大地帮助了我更好地理解 Promises
@VinKrish 认为这个问题被标记为重复,也可以关闭。节制万岁:/
@trincot 这正是我的想法。【参考方案2】:
您可以为此使用async/await
。我会解释更多,但实际上并没有什么。这只是一个常规的 for
循环,但我在构建 Promise 之前添加了 await
关键字
我喜欢这一点的是,您的 Promise 可以解析正常值,而不是像您的代码(或此处的其他答案)那样产生副作用。这为您提供了类似于塞尔达传说:通向过去中的力量,您可以在其中影响光明世界和黑暗世界中的事物 - 即,您可以轻松地在 Promised 数据可用之前/之后处理数据,而无需求助于深度嵌套的函数、其他笨拙的控制结构或愚蠢的IIFEs。
// where DarkWorld is in the scary, unknown future
// where LightWorld is the world we saved from Ganondorf
LightWorld ... await DarkWorld
这就是它的样子......
async function someProcedure (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(console.log)
.catch(console.error)
0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
done
看看我们如何不必在我们的过程中处理那个烦人的.then
调用?而async
关键字将自动确保返回Promise
,因此我们可以在返回值上链接.then
调用。这为我们取得了巨大的成功做好了准备:运行n
Promises 的序列,然后做一些重要的事情——比如显示成功/错误消息。
【讨论】:
await new Promise 无效 @androidDev 我不知道这是否是一个实际的 ecmascript 语法违规,但它在Chrome 58
中工作 - 像 await (expr)
这样的括号可用于解决歧义。我更新了问题以包含一个工作代码 sn-p。
@AndroidDev,你的说法是错误的。
如果你能把它写成一个命名函数,那就太棒了,那么我肯定会投票赞成。
@DanubioMüller 使用async
和await
?一些旧浏览器不支持它们【参考方案3】:
基于 trincot 的出色回答,我编写了一个可重用的函数,该函数接受一个处理程序来遍历数组中的每个项目。该函数本身返回一个承诺,允许您等待循环完成,并且您传递的处理函数也可能返回一个承诺。
循环(项目,处理程序):承诺
我花了一些时间才把它弄好,但我相信下面的代码可以在很多 Promise-looping 的情况下使用。
复制粘贴就绪代码:
// SEE https://***.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'))
错误处理
当异常发生时,我查看的许多 promise-looping 示例都会崩溃。让这个函数做正确的事情是相当棘手的,但据我所知,它现在正在工作。确保将 catch 处理程序添加到任何内部循环并在发生时调用拒绝函数。例如:
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 包中。
for-async
安装
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
)
)
有关详细信息,请参阅软件包自述文件。
【讨论】:
最好在循环函数中添加一些 cmets .. @kofifus 是的,你是对的。自从我写了这个答案,我实际上把这段代码变成了一个记录在案的 NPM 项目。我会将链接添加到答案中。 好吧,那里的代码似乎与这个完全不同...... @kofifus 是的,我重写了它。我喜欢重写代码并尝试进一步改进它:) 感谢您的回答!它解决了我的问题【参考方案4】:这是我的 2 美分:
可重用函数forpromise()
模拟经典的 for 循环
允许基于内部逻辑提前退出,返回一个值
可以收集传递给 resolve/next/collect 的结果数组
默认为 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, "]");
);
);
);
);
);
【讨论】:
不要在new Promise
回调中创建new Promise
。这是promise constructor antipattern 的放大案例。
另外,我认为 Promise 是为了避免 callback hell,但这里的嵌套随着每次测试而变得更深......【参考方案5】:
如果你仅限于 ES6,最好的选择是 Promise all。 Promise.all(array)
在成功执行 array
参数中的所有承诺后,还会返回一个承诺数组。
假设,如果要更新数据库中的很多学生记录,下面的代码演示了这种情况下 Promise.all 的概念-
let promises = students.map((student, index) =>
//where students is a db object
student.rollNo = index + 1;
student.city = 'City Name';
//Update whatever information on student you want
return student.save();
);
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方法也是同步的。所以,异步行为的问题不会出现在循环内部。
【讨论】:
如果您只是将map
视为forEach 而不会存储结果,为什么还要使用它?为什么不将地图分配给promises
并避免在其中使用push
?
OP 的要求是 "...确保每个 Promise 仅在另一个 Promise 之后运行...".
@vol7ron 我承认我也对使用map
i.s.o. 感到内疚。 forEach
... 输入更短,运行时性能差异通常在我的经验中并不重要。以上是关于JavaScript ES6承诺循环[重复]的主要内容,如果未能解决你的问题,请参考以下文章
Javascript ES6 会承诺支持'done' api吗?