JavaScript ES6承诺循环

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) ,尝试了返回承诺的简单函数,但最终总是归结为同一个问题: 循环是同步的

对于 异步,我只需使用 async.series()

你怎么解决这个问题?

215165 次浏览

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

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

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

  1. 使用for循环,以立即解析承诺开始

  2. 使用Array#reduce,以一个立即解析的承诺开始

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

  4. 与ECMAScript2017的__ABC0 / await语法

  5. 与ECMAScript2020的for await...of语法

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

Promisfying setTimeout

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

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

请参阅下面每个选项的代码片段和注释。

1. 与for

可以使用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只用于不丢失该链的跟踪,并允许循环的下一次迭代继续在同一链上。回调将在同步循环完成后开始执行。

重要的是,then-callback 返回delay()创建的承诺:这将确保异步链接。

2. 与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() );

当你实际一个数组的数据在承诺中使用时,这可能更有用。

3.使用一个函数将自身传递为resolution-callback

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

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立即调用。这是计数器和参数。如果计数器仍然低于10,函数将创建一个新的承诺,否则链接将停止。

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

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

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

你可以使用async/await。我可以解释更多,但其实没什么。它只是一个普通的for循环,但我在构造Promise之前添加了await关键字

我喜欢的是,你的承诺可以解析一个正常的值,而不是像你的代码(或这里的其他答案)所包含的副作用。这给了你像《塞尔达传说:通往过去》中那样的能力,你可以在光明世界而且和黑暗世界中影响事物——即,你可以在承诺的数据可用之前/之后轻松地处理数据,而不必借助于深度嵌套的函数、其他笨拙的控制结构或愚蠢的__abc2。

// 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,因此我们可以对返回值链式调用.then。这为我们的成功做好了准备:运行n承诺序列,然后做一些重要的事情——比如显示成功/错误消息。

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

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

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

可以复制粘贴的代码:

// 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包中。

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

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

以下是我的2美分价值:

  • 可重用函数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, "]");


});


});


});




});






});

如果你被限制在ES6,最好的选择是承诺所有。在成功执行array参数中的所有promise后,Promise.all(array)也返回一个promise数组。 假设要更新数据库中的许多学生记录,下面的代码演示了Promise的概念。在这种情况下-

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只是循环的一个示例方法。你也可以使用forforinforEach循环。概念很简单,开始你想要进行批量异步操作的循环。将每个这样的异步操作语句推入在该循环作用域之外声明的数组中。循环完成后,以这样的查询/承诺数组作为参数执行Promise all语句。

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

我在Angular中创建了一个代码片段,无限循环promise函数。您可以启动它、停止它或重新启动它。

你基本上需要递归调用相同的方法,并等待它的当前进程,如下所示:

async autoloop(): Promise<void> {
if(this.running){
await this.runMe();
await this.autoloop();
}
return Promise.resolve();
}

JavaScript:

import { Component } from '@angular/core';


@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
messages: string[] = [];
counter = 1;
running = false;


constructor() {
this.start();
}


onClick(): void {
this.running = !this.running;
if(this.running){
this.start();
}
else{
this.stop();
}
}


async onRestartClick(): Promise<void>{
await this.stop();
this.messages = [];
this.counter = 1;
this.start();
}


start(): void{
this.running = true;
this.autoloop();
}
  

async stop(): Promise<void>{
this.running = false;
await this.delay(1000);
}


async autoloop(): Promise<void> {
if(this.running){
await this.runMe();
await this.autoloop();
}
return Promise.resolve();
}


async runMe(): Promise<void> {
await this.delay(1000);
if(this.running){
this.messages.push(`Message ${this.counter++}`);
}
return Promise.resolve();
}


async delay(ms: number) {
await new Promise<void>((resolve) => setTimeout(() => resolve(), ms));
}
}

Html:

<h1>Endless looping a promise every 1 second</h1>
<button (click)="onClick()">Start / stop</button>
<button (click)="onRestartClick()">Restart</button>
<p *ngFor="let message of messages">
\{\{message}}
</p>

我看到上面的答案感到困惑。我用答案的灵感来编码以下内容。我觉得它的逻辑更明显,我调用函数替换原来的for循环:

async function pointToCountry(world, data) { // data is for loop array
if (data.length > 0) { // for condition
const da = data.shift(); // get current data and modified data one row code
// some business logic
msg = da.info
pointofView(world, da);
// await the current task
await new Promise(r => setTimeout(_ => {
r() // resolve and finish the current task
}, 5000))
// call itself and enter the next loop
pointToCountry(world, data)
} else { // business logic after all tasks
pointofView(world, { longitude: 0, latitude: 0 });
world.controls().autoRotate = true;
    

}
}

在Es6中,你应该使用for await

(async () => {
for await (const num of asyncIterable) {
console.log(num);
}
// my action here
})();

更多信息请访问https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of