JavaScript ES6 promise for loop

2022-08-30 01:29:37
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?)
}

The above will give the following random output:

6
9
4
8
5
1
7
2
3
0

The task is simple: Make sure each promise runs only after the other one ()..then()

For some reason, I couldn't find a way to do it.

I tried generator functions (), tried simple functions that return a promise, but at the end of the day it always comes down to the same problem: The loop is synchronous.yield

With async I'd simply use .async.series()

How do you solve it?


答案 1

As you already hinted in your question, your code creates all promises synchronously. Instead they should only be created at the time the preceding one resolves.

Secondly, each promise that is created with needs to be resolved with a call to (or ). This should be done when the timer expires. That will trigger any callback you would have on that promise. And such a callback (or ) is a necessity in order to implement the chain.new Promiseresolverejectthenthenawait

With those ingredients, there are several ways to perform this asynchronous chaining:

  1. With a loop that starts with an immediately resolving promisefor

  2. With that starts with an immediately resolving promiseArray#reduce

  3. With a function that passes itself as resolution callback

  4. With ECMAScript2017's async / await syntax

  5. With ECMAScript2020's for await...of syntax

But let me first introduce a very useful, generic function.

Promisfying setTimeout

Using is fine, but we actually need a promise that resolves when the timer expires. So let's create such a function: this is called promisifying a function, in this case we will promisify . It will improve the readability of the code, and can be used for all of the above options:setTimeoutsetTimeout

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

See a snippet and comments for each of the options below.

1. With for

You can use a loop, but you must make sure it doesn't create all promises synchronously. Instead you create an initial immediately resolving promise, and then chain new promises as the previous ones resolve:for

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

So this code creates one long chain of calls. The variable only serves to not lose track of that chain, and allow a next iteration of the loop to continue on the same chain. The callbacks will start executing after the synchronous loop has completed.thenp

It is important that the -callback returns the promise that creates: this will ensure the asynchronous chaining.thendelay()

2. With reduce

This is just a more functional approach to the previous strategy. You create an array with the same length as the chain you want to execute, and start out with an immediately resolving 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() );

This is probably more useful when you actually have an array with data to be used in the promises.

3. With a function passing itself as resolution-callback

Here we create a function and call it immediately. It creates the first promise synchronously. When it resolves, the function is called again:

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

This creates a function named , and at the very end of the code you can see it gets called immediately with argument 0. This is the counter, and the i argument. The function will create a new promise if that counter is still below 10, otherwise the chaining stops.loop

When resolves, it will trigger the callback which will call the function again.delay()then

4. With async/await

Modern JS engines support this syntax:

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

It may look strange, as it seems like the promises are created synchronously, but in reality the function returns when it executes the first . Every time an awaited promise resolves, the function's running context is restored, and proceeds after the , until it encounters the next one, and so it continues until the loop finishes.asyncawaitawait

5. With for await...of

With EcmaScript 2020, the for await...of found its way to modern JavaScript engines. Although it does not really reduce code in this case, it allows to isolate the definition of the random interval chain from the actual iteration of it:

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

答案 2

You can use for this. I would explain more, but there's nothing really to it. It's just a regular loop but I added the keyword before the construction of your Promiseasync/awaitforawait

What I like about this is your Promise can resolve a normal value instead of having a side effect like your code (or other answers here) include. This gives you powers like in The Legend of Zelda: A Link to the Past where you can affect things in both the Light World and the Dark World – ie, you can easily work with data before/after the Promised data is available without having to resort to deeply nested functions, other unwieldy control structures, or stupid IIFEs.

// where DarkWorld is in the scary, unknown future
// where LightWorld is the world we saved from Ganondorf
LightWorld ... await DarkWorld

So here's what that will look like ...

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

See how we don't have to deal with that bothersome call within our procedure? And keyword will automatically ensure that a is returned, so we can chain a call on the returned value. This sets us up for great success: run the sequence of Promises, then do something important – like display a success/error message..thenasyncPromise.thenn