JavaScript ES6承诺循环

Pon*_*oni 103 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?)
}
Run Code Online (Sandbox Code Playgroud)

以上将给出以下随机输出:

6
9
4
8
5
1
7
2
3
0
Run Code Online (Sandbox Code Playgroud)

任务很简单:确保每个promise只在另一个(.then())之后运行.

出于某种原因,我找不到办法.

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

使用异步我只是使用async.series().

你是如何解决的?

tri*_*cot 259

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

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

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

  1. 随着await与一个立即解决的承诺开始循环

  2. 随着for与一个立即解决的承诺开始

  3. 使用一个在解决时调用自身的函数

  4. 使用ECMAScript2017的Array#reduce/ async语法

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

随着 await

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

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)
    ));
}
Run Code Online (Sandbox Code Playgroud)

随着 for

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

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

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

3.在解决问题时自行调用的函数

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

(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);
Run Code Online (Sandbox Code Playgroud)

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

调用reduce将触发loop将再次调用该函数的回调.resolve()只是一种不同的说法then.

4.带loop.bind(null, i+1)/_ => loop(i+1)

现代JS引擎支持这种语法:

(async function loop() {
    for (let i = 0; i < 10; i++) {
        await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
        console.log(i);
    }
})();
Run Code Online (Sandbox Code Playgroud)

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

因为基于超时返回承诺可能是常见的事情,您可以创建一个单独的函数来生成这样的承诺.在这种情况下,这称为promisifying函数await.它可以提高代码的可读性:

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);
    }
})();
Run Code Online (Sandbox Code Playgroud)

  • 我只是想知道这个家伙是如何提出这么多解决方案的。 (5认同)

Tha*_*you 12

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

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

// where DarkWorld is in the scary, unknown future
// where LightWorld is the world we saved from Ganondorf
LightWorld ... await DarkWorld
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

看看我们如何.then在我们的程序中处理这个麻烦的电话?而async关键字会自动确保Promise返回,所以我们可以链.then上的返回值调用.这为我们取得了巨大的成功:运行nPromise 序列,然后做一些重要的事情 - 比如显示成功/错误消息.

  • @AndroidDev我不知道它是否是一个真正的ecmascript语法违规,但它在`Chrome 58`中工作 - 括号如`await(expr)`可以用来解决歧义.我更新了问题以包含一个有效的代码片段. (3认同)
  • @DanubioMüller 使用“async”和“await”?一些旧浏览器不支持它们 (3认同)
  • @AndroidDev,你的说法是错误的。 (2认同)

Sti*_*itt 8

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

循环(项目,处理程序):承诺

我花了一些时间才把它弄好,但我相信下面的代码可以在很多 promise 循环的情况下使用。

复制粘贴就绪代码:

// SEE /sf/answers/3240653461/
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)
}
Run Code Online (Sandbox Code Playgroud)

用法

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

// 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)
}
Run Code Online (Sandbox Code Playgroud)

高级用例

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

处理程序(当前,索引,所有)

处理程序传递了 3 个参数。当前项、当前项的索引和被循环的完整数组。如果处理函数需要做异步工作,它可以返回一个承诺,循环函数将在开始下一次迭代之前等待承诺解决。您可以嵌套循环调用,一切都按预期工作。

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!'))
Run Code Online (Sandbox Code Playgroud)

错误处理

当异常发生时,我看到的许多承诺循环示例都会崩溃。让这个函数做正确的事情非常棘手,但据我所知它现在正在工作。确保向任何内部循环添加一个 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)
  return loop(test, (testCase) => {
    console.info(testCase)
  })
  .then(testNext)
  .catch(testFailed)
}))
.then(() => console.info('All tests done'))
Run Code Online (Sandbox Code Playgroud)

更新:NPM 包

自从写了这个答案,我把上面的代码放到了一个 NPM 包中。

异步

安装

npm install --save for-async
Run Code Online (Sandbox Code Playgroud)

进口

var forAsync = require('for-async');  // Common JS, or
import forAsync from 'for-async';
Run Code Online (Sandbox Code Playgroud)

用法(异步)

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
  })
})
Run Code Online (Sandbox Code Playgroud)

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

  • @kofifus 是的,你是对的。自从我写了这个答案,我实际上把这段代码变成了一个文档化的 NPM 项目。我将添加链接到答案。 (2认同)

Srk*_*k95 6

如果您仅限于 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
});
Run Code Online (Sandbox Code Playgroud)

Map 只是 for 循环的一个示例方法。您还可以使用fororforinforEach循环。所以这个概念非常简单,启动您想要执行批量异步操作的循环。将每个此类异步操作语句推送到在该循环范围之外声明的数组中。循环完成后,使用准备好的此类查询/承诺数组作为参数执行 Promise all 语句。

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

  • OP 的要求是 *“...确保每个承诺仅在另一个承诺之后运行...”*。 (4认同)