js execution sequence, event loop

1, js execution mechanism

As we all know, JavaScript is a single threaded language. Because it is a single thread, the code should be executed from top to bottom. Is this the case? Let's look at the following code first:

  setTimeout(() => {
    console.log('set1')
  });

  new Promise((resolve, reject) => {
    console.log('p1');
    resolve();
  }).then(() => {
    console.log('then1')
  });

  console.log(1);

 // Output: p1 1 then1 set1

The output order of the above code is disrupted. Why the output order is not in the order of the code, the following part will solve your doubts.

1. Task queue

All tasks can be divided into synchronous tasks and asynchronous tasks. Synchronous tasks, as the name suggests, are tasks to be executed immediately. Generally, synchronous tasks will be directly executed in the main thread; Asynchronous tasks are tasks executed asynchronously, such as ajax network requests and setTimeout timing functions. Asynchronous tasks are coordinated through the mechanism of Event Queue.

2. Event cycle

Synchronous and asynchronous tasks enter different execution environments respectively. Synchronous tasks enter the main thread, that is, the main execution stack, and asynchronous tasks enter the Event Queue. If the task in the main thread is empty after execution, the corresponding task will be read from the Event Queue and pushed into the main thread for execution. The continuous repetition of the above process is what we call Event Loop.

3. Macro task and micro task

The two tasks are in the macro queue and micro queue in the task queue respectively;
Macro queue and micro queue form task queue;
The task queue puts the task into the execution stack for execution;

Macro task

  • Including overall code script
  • setTimeout
  • setInterval
  • setImmediate (unique to Node)
  • I/O
  • UI interaction event (unique to browser)
  • requestAnimationFrame (browser only)

Micro task

  • Promise
  • Process.nexttick (unique to node)
  • MutationObserver (listening for DOM tree changes)
  • Object.observe (asynchronous monitoring object modification, obsolete)

example:

 console.log('1: script start');

 setTimeout(() => {
   console.log('2: setTimeout1');
   new Promise((resolve) => {
     console.log('3: promise1');
     resolve();
   }).then(() => {
     console.log('4: then1')
   })
 });

 new Promise((resolve) => {
   console.log('5: promise2')
   resolve();
 }).then(() => {
   console.log('6: then2');
   setTimeout(() => {
     console.log('7: setTimeout2')
   })
 })

 console.log('8: script end')

The first round of event cycle flow is as follows:

  • As the first macro task, the overall script enters the main thread, encounters console, and outputs 1: script start.
  • When setTimeout is encountered, its callback function is distributed to the macro task event queue. We temporarily mark it as setTimeout1.
  • When promise is encountered, new Promise is executed directly, and output 5: promise 2, then is distributed to the micro task queue,
  • console encountered, output 8: script end
Macro task Event QueueMicro task Event Queue
setTimeout1then2
  • The above table shows the status of each Event Queue at the end of the first round of event loop macro task. At this time, 1, 5 and 8 have been output.
  • We found that then2 micro task executes output 6: then2, encounters setTimeout, distributes it to the macro task queue, marks it as setTimeout2, and the first round of event loop ends.
  • The first cycle officially ends and outputs 1, 5, 8 and 6.

The second round of events starts from the setTimeout1 macro task:

Macro task Event QueueMicro task Event Queue
setTimeout1
setTimeout2
  • First, output 2: setTimeout1, then encounter Promise, immediately execute new Promise, output 3: Promise 1, distribute then to the micro task queue and mark then1. The execution of setTimeout1 ends, and the events in the queue are as follows:
Macro task Event QueueMicro task Event Queue
setTimeout2then1
  • After the second round of macro task execution, we found that then1 micro task can be executed.
  • Output 4: then1.
  • At the end of the second cycle, the second cycle outputs 2, 3 and 4.

The third round of event loop starts. At this time, only setTimeout2 is left for execution.

  • Direct output 7: setTimeout2

The whole code has carried out three event cycles, and the complete output is 1, 5, 8, 6, 2, 3, 4 and 7

async/await (key)

  console.log(1);

  async function fn(){
      console.log(2);
      await console.log(3);
      console.log(4);
  }

  setTimeout(()=>{
      console.log(5);
  },0)

  fn();

  new Promise((resolve)=>{
      console.log(6);
      resolve();
  }).then(()=>{
      console.log(7);
  })

  console.log(8);

async

When we use async before the function, the function returns a Promise object:

async function test() {
    return 1   // async's function will help us implicitly use Promise.resolve(1) here
}

// Equivalent to the following code
function test() {
   return new Promise(function(resolve, reject) {
       resolve(1)
   })
}

It can be seen that async is just a syntax sugar, which just helps us return a Promise

Immediate execution in Promise and async

We know that asynchrony in Promise is embodied in then and catch, so the code written in Promise is executed immediately as a synchronous task. In async/await, the code is executed immediately before await appears. So what happened when await appeared?

await

await means wait. It is the result of the "expression" on the right. The evaluation result of this expression can be Promise object or other values (in other words, there is no special restriction). And can only be used internally with async

Many people think that await will wait until the following expression is executed before continuing to execute the following code. In fact, await is a sign of giving up the thread. The expression after await will be executed first. Add the code after await to the microtask, and then jump out of the whole async function to execute the code.

In a popular way:

When using await, it will be executed from right to left. When await is encountered, it will block the code behind it in the function to execute the synchronization code outside the function. When the external synchronization code is executed, it will return to the function to execute the remaining code. After await is executed, it will first process the code of the micro task queue

Revise the previous interview questions:

  console.log(1);

  async function fn(){
    console.log(2)
    new Promise((resolve)=>{
        resolve();
    }).then(()=>{
        console.log("XXX")
    })
    await console.log(3)
    console.log(4)
  }

  fn();

  new Promise((resolve)=>{
      console.log(6)
      resolve();
  }).then(()=>{
      console.log(7)
  })

  console.log(8)

// The execution result is: 1 2 3 6 8 XXX 4 7

Revision 2

  console.log(1);

  new Promise((resolve)=>{
    resolve();
  }).then(()=>{
    console.log("XXX")
  })

  async function fn(){
    console.log(2)
    await console.log(3)
    console.log(4)
    new Promise((resolve)=>{
        resolve();
    }).then(()=>{
        console.log("YYY")
    })
  }

  fn();

  new Promise((resolve)=>{
    console.log(6)
    resolve();
  }).then(()=>{
    console.log(7)
  })

  console.log(8)

// The execution result is 1 2 3 6 8 XXX 4 7 YYY

It can be seen that when the code is executed, as long as you encounter await, the code behind await will be put into the micro task queue after the current await is executed.

Back to the original interview question of the article, add await in front of console.log(4). Can 4 be printed after 3?

console.log(1);
async function fn(){
  console.log(2)
  await console.log(3)
  await console.log(4)    
}
setTimeout(()=>{
  console.log(5)
},0)
fn();
new Promise((resolve)=>{
  console.log(6)
  resolve();
}).then(()=>{
  console.log(7)
})
console.log(8)

// The results are: 1 2 3 6 8 4 7 5

It can be seen that during code execution, as long as you encounter await and execute the current await code (i.e. await console.log(3)), the code after await (i.e. await console.log(4)) will be placed in the micro task queue.

What if you add other code of await after await console.log(4)?

console.log(1);
async function fn(){
    console.log(2)
    await console.log(3)
    await console.log(4)
    await console.log("await Subsequent:",11)
    await console.log("await Subsequent:",22)
    await console.log("await Subsequent:",33)
    await console.log("await Subsequent:",44)
}
setTimeout(()=>{
    console.log(5)
},0)
fn();
new Promise((resolve)=>{
    console.log(6)
    resolve();
}).then(()=>{
    console.log(7)
})
console.log(8)

/**
 * The execution results are:
 * 1
 * 2
 * 3
 * 6
 * 8
 * 4
 * 7
 * await After: 11
 * await After: 22
 * await After: 33
 * await After: 44
 * 5
 */

It can be seen that when we constantly meet await and continuously put the code after await into the micro task queue, the code execution sequence is to complete the execution of the micro task queue before executing the code in the macro task queue.

Keywords: Javascript Front-end

Added by anindya23 on Wed, 01 Dec 2021 13:17:42 +0200