Nguyễn Văn Phong
Nguyễn Văn Phong

Reputation: 14198

ES6 variable scopes in loops with await inside

As we may know, var keyword defines a variable globally, or locally to an entire function regardless of block scope. So the below code will log 5 times with the same value.

for(var i = 0; i < 5; i++){
  setTimeout(() => console.log(i), 2000);
}

To visualize the above JS runtime like this enter image description here As you can see, 5 tasks in Callback Queue will wait until Call stack is empty. So after the synchronous loop is done - It means Call stack is empty in my case, then 5 scheduled tasks - console.log(i) with the value of i is equal to 5 will be executed. You can play around here

And what I want is to log right after i == 2. It works as I expected.

var sleep = (ms) => new Promise(resolve => setTimeout(() => resolve(1), ms));
async function runAsync(){
  for(var i = 0; i < 5; i++){
    if(i == 2) await sleep(2000);
    setTimeout(() => console.log(i));
  }
}

runAsync();

But I'm curious how it works while I'm not sure the call stack is empty? Whether when I'm using await, allowing the caller of the async function to resume execution, or some else?

Any explanation/visualization of image flow would be appreciated. Thanks.

Upvotes: 3

Views: 2975

Answers (1)

JLRishe
JLRishe

Reputation: 101652

await cedes control of the thread and allows other processes to run until the promise being awaited resolves. Even if the promise is already resolved, await will yield to any "microtasks" that have been waiting to execute, but that's a moot point in your case because your promise takes a full two seconds to resolve.

In your case, two setTimeouts are queued up before the await, so they are allowed to run when the await happens.

The timeline is basically like this:

  • i = 0
  • setTimeout 1 scheduled
  • i = 1
  • setTimeout 2 scheduled
  • i = 2
  • await
  • setTimeout 1 callback runs
  • setTimeout 2 callback runs
  • setTimeout 3 scheduled
  • i = 3
  • setTimeout 4 scheduled
  • i = 4
  • setTimeout 5 scheduled
  • i = 5
  • loop ends
  • setTimeout 3 callback runs
  • setTimeout 4 callback runs
  • setTimeout 5 callback runs

You can see that i is 2 when the first pair of setTimeouts are allowed to execute, and it is 5 when the remaining 3 execute.

Here is a snippet that hopefully demonstrates the process a little better:

var sleep = (ms) => new Promise(resolve => setTimeout(() => resolve(1), ms));
async function runAsync() {
  for (var i = 0; i < 5; i++) {
    console.log('i is now', i);
    if (i == 2) {
      console.log('about to sleep');
      await sleep(5000);
      console.log('now done sleeping');
    }
    console.log('about to setTimeout. i is', i, 'right now');
    setTimeout(() => {
        console.log('setTimeout task running:', i, '- scheduling a new timeout.');
        setTimeout(() => console.log('inner timeout:', i), 1000);
    });
  }
  console.log('done looping. i is', i);
}

runAsync();

Upvotes: 5

Related Questions