JavaScript ES6承诺循环

时间:2016-10-30 12:15:00

标签: javascript es6-promise

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

你是如何解决的?

5 个答案:

答案 0 :(得分:201)

正如您在问题中已经暗示的那样,您的代码会同步创建所有承诺。相反,它们只应在前一个结算时创建。

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

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

  1. for循环以立即解决的承诺开头

  2. Array#reduce以立即解决的承诺

  3. 开头
  4. 使用将自身作为分辨率回调

  5. 的函数
  6. 使用ECMAScript2017&#39; async / await syntax

  7. 建议使用ECMAScript2020 for await...of syntax

  8. 请参阅以下每个选项的摘要和评论。

    1。使用for

    可以使用for循环,但您必须确保它不会同步执行new Promise。相反,你创建一个初始立即解决的承诺,然后链接新的承诺,因为前面的承诺解决:

    &#13;
    &#13;
    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;
    &#13;
    &#13;

    2。使用reduce

    这只是对以前策略更具功能性的方法。您创建一个与您要执行的链长度相同的数组,并从一个立即解决的承诺开始:

    &#13;
    &#13;
    [...Array(10)].reduce( (p, _, i) => 
        p.then(_ => new Promise(resolve =>
            setTimeout(function () {
                console.log(i);
                resolve();
            }, Math.random() * 1000)
        ))
    , Promise.resolve() );
    &#13;
    &#13;
    &#13;

    当您实际拥有一个数据以便在promises中使用时,这可能更有用。

    3。使用函数将自身作为resolution-callback

    传递

    这里我们创建一个函数并立即调用它。它同步创造了第一个承诺。解析后,再次调用该函数:

    &#13;
    &#13;
    (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;
    &#13;
    &#13;

    这将创建一个名为loop的函数,在代码的最后,您可以看到它会立即被参数0调用。这是计数器,以及 i 参数。如果该计数器仍然低于10,该函数将创建一个新的承诺,否则链接将停止。

    resolve()的调用将触发then回调,该回调将再次调用该函数。 loop.bind(null, i+1)只是_ => loop(i+1)的另一种说法。

    4。使用async / await

    现代JS引擎support this syntax

    &#13;
    &#13;
    (async function loop() {
        for (let i = 0; i < 10; i++) {
            await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
            console.log(i);
        }
    })();
    &#13;
    &#13;
    &#13;

    它可能看起来很奇怪,因为似乎就像new Promise()调用是同步执行的,但实际上async函数会返回执行第一个await。每当等待的承诺解析时,函数的运行上下文将被恢复,并在await之后继续,直到遇到下一个,然后一直持续到循环结束。

    由于基于超时返回承诺可能是常见的事情,您可以创建一个单独的函数来生成这样的承诺。这称为 promisifying 一个函数,在本例中为setTimeout。它可以提高代码的可读性:

    &#13;
    &#13;
    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;
    &#13;
    &#13;

    5。使用for await...of

    最近,for await...of语法找到了一些JavaScript引擎。虽然在这种情况下它并没有真正减少代码,但它允许将随机区间链的定义与其实际迭代隔离开来:

    &#13;
    &#13;
    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;
    &#13;
    &#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

所以这就是看起来像......

&#13;
&#13;
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;
&#13;
&#13;

了解我们如何处理我们程序中令人烦恼的.then电话? async关键字会自动确保返回Promise,因此我们可以对返回的值进行.then链接。这为我们取得了巨大的成功:运行n Promises的序列,然后做一些重要的事情 - 比如显示成功/错误消息。

答案 2 :(得分:3)

基于trincot的优秀答案,我编写了一个可重用的函数,它接受一个处理程序来遍历数组中的每个项目。函数本身返回一个promise,它允许你等到循环结束,你传递的处理函数也可以返回一个promise。

loop(items,handler):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包。

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

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

答案 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只是循环的示例方法。您还可以使用forforinforEach循环。因此,此概念非常简单,请启动要执行批量异步操作的循环。将每个此类异步操作语句推送到该循环范围之外声明的数组中。循环完成后,以准备好的查询/承诺数组作为参数执行Promise all语句。

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

答案 4 :(得分:0)

这是我的2美分价值:

  • resuable function 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, "]");

                    });

                });

            });


        });



    });