JavaScript ES6承诺循环[重复]

问题描述 投票:87回答:5

这个问题在这里已有答案:

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

你是如何解决的?

javascript es6-promise
5个回答
217
投票

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

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

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

  1. 使用for循环,以立即解决的承诺开始
  2. 随着Array#reduce开始立即解决承诺
  3. 使用一个将自身作为分辨率回调传递的函数
  4. 使用ECMAScript2017的async / await syntax
  5. 建议使用ECMAScript2020的for await...of syntax

请参阅下面的每个选项的摘要和注释。

1. With 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)
    ));
}

2. With reduce

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

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

当您实际拥有要在promise中使用的数据的数组时,这可能更有用。

3. With a function passing itself as resolution-callback

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

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

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

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

4. With 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);
    }
})();

它可能看起来很奇怪,因为看起来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);
    }
})();

5. With 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);
})();

10
投票

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

我喜欢这个是你的Promise可以解决一个正常的值,而不是像你的代码(或其他答案)包括副作用。这为你提供了“塞尔达传说:过去的链接”中的力量,你可以在光明世界和黑暗世界中影响事物 - 也就是说,你可以在承诺数据可用之前/之后轻松处理数据,而不必诉诸于深层嵌套的功能,其他笨拙的控制结构,或愚蠢的IIFEs。

// 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

看看我们如何在我们的程序中处理令人讨厌的.then电话?并且async关键字将自动确保返回Promise,因此我们可以在返回值上链接.then调用。这为我们取得了巨大的成功:运行n Promises的序列,然后执行一些重要的事情 - 比如显示成功/错误消息。


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

Usage

要使用它,请使用数组调用它作为第一个参数循环,将处理函数作为第二个参数循环。不传递第三,第四和第五个参数的参数,它们在内部使用。

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!'))

Advanced use cases

让我们看一下处理函数,嵌套循环和错误处理。

处理程序(当前,索引,全部)

处理程序传递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函数。例如。:

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

UPDATE: NPM package

自写这个答案以来,我将上述代码改为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
  })
})

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


0
投票

这是我的2美分价值:

  • 可重复使用的功能qazxsw poi
  • 模仿经典的for循环
  • 允许基于内部逻辑提前退出,返回一个值
  • 可以收集传递给resolve / next / collect的结果数组
  • 默认为start = 0,increment = 1
  • 在循环内抛出的异常被捕获并传递给.catch()

forpromise()

0
投票

如果您仅限于ES6,最好的选择是Promise all。在成功执行 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, "]"); }); }); }); }); });参数中的所有承诺后,Promise.all(array)也会返回一组promise。假设,如果你想更新数据库中的许多学生记录,下面的代码在这种情况下演示了Promise.all的概念 -

array

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

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

© www.soinside.com 2019 - 2024. All rights reserved.