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())。
.then()
由于某种原因,我找不到办法做到这一点。
我尝试过生成yield器 函数 (
yield
使用async我会简单地使用async.series().
async.series()
你如何解决它?
正如您在问题中已经暗示的那样,您的代码会同步创建所有承诺。相反,它们应该只在前一个解决时创建。
其次,每个创建的 Promise 都new Promise需要通过调用resolve(or reject) 来解决。这应该在计时器到期时完成。这将触发then您对该承诺的任何回调。为了实现链,这样的then回调(或)是必要的。await
new Promise
resolve
reject
then
await
有了这些成分,有几种方法可以执行这种异步链接:
循环以for立即解决的承诺开始
for
从Array#reduce立即解决的承诺开始
Array#reduce
使用将自身作为解析回调传递的函数
使用 ECMAScript2017 的async/await语法
async
使用 ECMAScript2020 的for await...of语法
for await...of
但让我先介绍一个非常有用的通用函数。
setTimeout
使用setTimeout很好,但我们实际上需要一个在计时器到期时解决的承诺。所以让我们创建这样一个函数:这被称为 promisifying 一个函数,在这种情况下我们将 promisify setTimeout。它将提高代码的可读性,并可用于上述所有选项:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
请参阅以下每个选项的片段和评论。
您 可以 使用for循环,但必须确保它不会同步创建所有 Promise。相反,您创建一个初始的立即解决承诺,然后在之前的承诺解决时链接新的承诺:
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仅用于不丢失该链的跟踪,并允许循环的下一次迭代在同一链上继续。回调将在同步循环完成后开始执行。
p
重要的是then-callback 返回 创建的承诺delay():这将确保异步链接。
delay()
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 中使用的数据的数组时,这可能更有用。
这里我们创建一个函数并立即调用它。它同步创建第一个承诺。当它解析时,再次调用该函数:
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,否则链接将停止。
loop
解决后delay(),它将触发then回调,该回调将再次调用该函数。
现代 JS 引擎支持这种语法:
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); } })();
这可能看起来很奇怪,因为promise 似乎 是同步创建的,但实际上async函数在执行 first 时 返回await。每次等待的 promise 解决时,函数的运行上下文都会恢复,并在 之后继续await,直到遇到下一个,所以它继续直到循环结束。
借助 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); })();