JavaScript ES6 Promise循环

IT技术 javascript es6-promise
2021-01-20 23:14:48
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 只在另一个 ( .then())之后运行

出于某种原因,我找不到办法做到这一点。

我尝试了生成器函数 ( yield),尝试了返回Promise的简单函数,但在一天结束时,它总是归结为相同的问题:循环是同步的

使用async我只会使用async.series().

你怎么解决?

5个回答

正如您在问题中已经暗示的那样,您的代码同步创建所有Promise。相反,它们应该只在前一个解析时创建。

其次,创建的每个Promise都new Promise需要通过调用resolve(或reject来解决这应该在计时器到期时完成。这将触发then您对该Promise的任何回调。而这样的then回调(或await)是实现链所必需的。

有了这些成分,有几种方法可以执行这种异步链接:

  1. 使用以for立即解决Promise开始循环

  2. 随着Array#reduce与一个立即解决的Promise开始

  3. 使用将自身作为分辨率回调传递的函数

  4. 使用 ECMAScript2017 的async/await语法

  5. 使用 ECMAScript2020 的for await...of语法

但是让我先介绍一个非常有用的通用函数。

Promise setTimeout

使用setTimeout很好,但我们实际上需要一个在计时器到期时解决的Promise。所以让我们创建一个这样的函数:这​​被称为promisifying一个函数,在这种情况下我们将 promisify setTimeout它将提高代码的可读性,并可用于上述所有选项:

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

查看下面每个选项的片段和评论。

1.与 for

可以使用for循环,但必须确保它不会同步创建所有Promise。相反,您创建一个初始的立即解决的Promise,然后在之前的Promise解决时链接新的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仅用于不丢失对该链的跟踪,并允许循环的下一次迭代在同一链上继续。回调将在同步循环完成后开始执行。

then-callback返回delay()创建的Promise很重要:这将确保异步链接。

2.与 reduce

这只是对先前策略的一种更实用的方法。您创建一个与要执行的链具有相同长度的数组,并从立即解决的Promise开始:

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. 将自身作为分辨率回调传递的函数

在这里我们创建一个函数并立即调用它。它同步创建第一个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,否则链接将停止。

delay()解决时,它将触发then回调,回调将再次调用该函数。

4. 与async/await

现代 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函数返回时,执行第一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);
})();

我只是想知道这家伙是怎么想出这么多解决方案的。
2021-03-14 23:14:48
@trincot 这正是我的想法。
2021-03-17 23:14:48
@VinKrish 并认为这个问题被标记为重复,也可以关闭。节制万岁:/
2021-03-29 23:14:48
@trincot 非常感谢你!这个答案极大地帮助了我更好地理解 Promises
2021-03-30 23:14:48
最后的方法是完美的,我们只需要添加async与外部功能和awaitPromises内环路
2021-04-03 23:14:48

您可以async/await为此使用我会解释更多,但没有什么真正的。这只是一个常规for循环,但我await在构建 Promise 之前添加了关键字

我喜欢这一点的是您的 Promise 可以解析正常值,而不是像您的代码(或此处的其他答案)包含的副作用。这为您提供了《塞尔达传说:过去的链接》中的力量,您可以在其中影响光明世界黑暗世界中的事物- 即,您可以在Promise数据可用之前/之后轻松处理数据,而无需诉诸深度嵌套的函数、其他笨拙的控制结构或愚蠢的IIFE

// 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返回 a,因此我们可以.then对返回值进行链式调用。这让我们取得了巨大的成功:运行nPromise序列然后做一些重要的事情——比如显示成功/错误消息。

如果你能把它写成一个命名函数,那就太棒了,那么我肯定会投票给它。
2021-03-11 23:14:48
约翰,上面的函数命名了。someProcedure.name返回"someProcedure"你可能的意思是一个函数声明转换很简单:更改const someProcedure = async n => ...async function someProcedure (n) ...
2021-03-15 23:14:48
等待新的Promise无效
2021-03-23 23:14:48
@AndroidDev 我不知道这是否是实际的 ecmascript 语法违规,但它在这里工作Chrome 58- 括号 likeawait (expr)可用于解决歧义。我更新了问题以包含一个工作代码片段。
2021-03-25 23:14:48
@AndroidDev,您的说法是错误的。
2021-03-28 23:14:48

基于 trincot 的出色回答,我编写了一个可重用的函数,该函数接受一个处理程序来运行数组中的每个项目。该函数本身返回一个Promise,允许您等待直到循环完成并且您传递的处理程序函数也可能返回一个Promise。

循环(项目,处理程序):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'))

错误处理

我看到的许多Promise循环示例在发生异常时都会崩溃。让这个函数做正确的事情非常棘手,但据我所知它现在正在工作。确保向任何内部循环添加一个 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 包中。

异步

安装

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
  })
})

有关更多详细信息,请参阅包自述文件。

@kofifus 是的,我重写了它。我喜欢重写代码并尝试进一步改进它:)
2021-03-10 23:14:48
感谢您的回答!它解决了我的问题
2021-03-10 23:14:48
好的,那里的代码似乎与这个完全不同......
2021-03-15 23:14:48
@kofifus 是的,你是对的。自从我写了这个答案,我实际上把这段代码变成了一个文档化的 NPM 项目。我会将链接添加到答案中。
2021-04-07 23:14:48
将一些注释添加到循环函数会很好..
2021-04-08 23:14:48

如果您仅限于 ES6,最好的选择是 Promise all。Promise.all(array)在成功执行array参数中的所有Promise后,还会返回一组Promise假设,如果你想更新数据库中的很多学生记录,下面的代码演示了这种情况下 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 只是循环的一个示例方法。您也可以使用fororforinforEach循环。所以这个概念非常简单,启动你想要执行批量异步操作的循环。将每个此类异步操作语句推送到在该循环范围之外声明的数组中。循环完成后,使用此类查询/Promise的准备数组作为参数执行 Promise all 语句。

基本概念是 javascript 循环是同步的,而数据库调用是异步的,我们在循环中使用 push 方法也是同步的。所以,异步行为的问题不会发生在循环内部。

@vol7ron 我承认我也对使用mapiso感到内疚forEach......输入时间较短,而且运行时性能差异通常在我的经验中并不重要。
2021-03-22 23:14:48
map如果你只是把它当作forEach而不打算存储结果,为什么要使用为什么不将地图分配给内部promises并避免push内部?
2021-04-04 23:14:48
OP 的要求是“......确保每个Promise只在另一个Promise之后运行......”
2021-04-05 23:14:48

这是我的 2 美分:

  • 可重用函数 forpromise()
  • 模拟经典的 for 循环
  • 允许基于内部逻辑提前退出,返回一个值
  • 可以收集传递给 resolve/next/collect 的结果数组
  • 默认为开始=0,增量=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, "]");

                    });

                });

            });


        });



    });

另外,我认为 Promises 是为了避免回调地狱,但是这里的嵌套随着每次测试变得更深......
2021-03-11 23:14:48
不要new Promisenew Promise回调内部创建一个这是promise 构造函数反模式的放大案例
2021-03-28 23:14:48