I wrote an article about Event Loop before. However, I found that the understanding of some concepts at that time was wrong in the subsequent in-depth study, so I revised it and updated it.

Javascript is a single-threaded language with an asynchronous, non-blocking execution style. In many cases, events and callback functions are required to drive javascript. When are these registered callback functions called by the runtime environment, and in what order are they executed? This leads to a mechanism called Event Loop.

What is JS Event Loop

JS Event Loop is a message communication mechanism running in the browser/Node environment. It is an independent thread outside the main thread. When the main thread needs to perform some time-consuming operation that might block the thread (such as request send and receive response, file I/O, data calculation), the main thread registers a callback function and throws it to the Event Loop thread to listen, and continues to execute. Once a message is returned and the main thread is idle, The Event Loop notifies the main thread in time and executes the corresponding callback function to obtain the information, thus achieving the purpose of non-blocking.

Execution stack and message queue

Before we parse the Event Loop mechanism, we need to understand the concepts of stack and queue. Stack and queue, both are linear structures, but the stack follows last in first off LIFO, with an open back cover. The queue follows fisRT in first out (FIFO), with both ends transparent.

The container environment on which the Event Loop relies for its successful execution is related to these two concepts.

As we know, in the process of JS code execution, an “execution context (execution environment/scope)” of the current environment will be generated, which is used to store variables in the current environment. After this context is generated, it will be pushed into the JS execution stack. Once the execution is complete, the execution context is popped by the execution stack and the associated variables are destroyed, freeing up the memory occupied by the variables in the environment when the next garbage collection comes around.

This execution stack, you can also think of it as a single thread of JavaScript, where all the code is running through it, executing in sequence synchronously, or blocking, and that’s the synchronous scenario.

What about asynchronous scenarios? There is an obvious need for a container separate from the “execution stack” to manage these asynchronous states, so there is a Task queue structure outside the “main thread” and “execution stack” to manage asynchronous logic. All callbacks for asynchronous operations are temporarily stuffed into this queue. Event Loop in between, the role of a housekeeper, it will be at a fixed time interval is continuously polling, when it is found that the main thread free, she will go into the Task queue with an asynchronous callback, put it into execution stack, after a period of time, the main thread execution is completed, the pop-up context, free again, The Event Loop will do the same again… Cycle in turn, thus forming a complete set of event cycle operation mechanism.

The diagram above illustrates the process more succinctly, but with the addition of heap, heap and stack. In simple terms, heap is the memory space allocated by the developer, while stack is the memory space used by the native compiler, and the two are independent.

Microtask and macrotask

The previous section will suffice if you just want to handle a more mundane interview, but to answer the following interview question, you must delve into the Event Loop again to understand the underlying principles of task queues: microTasks and MacroTasks.

// Print the order of log after executing the following code
console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')}async function async2() {
  console.log('async2 end')
}
async1()

setTimeout(function() {
  console.log('setTimeout')},0)

new Promise(resolve= > {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')})console.log('script end')

// Log Print order: script start -> async2 end -> Promise -> script end -> asynC1 end -> promise1 -> promise2 -> setTimeout
Copy the code

If there is only a single Task queue, there is no order problem. However, the fact is that the browser will load different task sources into different queues according to the nature of the task. Task sources can be divided into microtasks and macrotasks. Because of the browser’s mechanism of reading callback functions in the two different task source queues, the execution order problem in the above code is caused by the mechanism.

The microtasks include process.nextTick, Promise, and MutationObserver, where process.nextTick is unique to Node.

Macro tasks include Script, setTimeout, setInterval, setImmediate, I/O, and UI Rendering.

Browser Event Loop execution mechanism

Now that we understand the concept of microtasks macro tasks, we can fully analyze the execution mechanism of one side of the Event Loop.

  • Initial state: The stack is empty,micro-taskIs empty,macro-taskThere is one and only onescriptScript (overall code)
  • scriptScript execution: The global context (script task) is pushed onto the execution stack, and the code executes accordingly in a synchronous manner. In the process of execution, new ones may be generatedmacro-taskmicro-taskThey are pushed into their respective task queues
  • scriptScript outqueue: synchronization code is finished,scriptThe script will be removedmacro-task, this process is essentially the process of executing and dequeuing macro tasks.
  • Microtask queue execution: The previous step has changedscriptThe macro task executes and exits the queue, and the stack is empty.Event LoopWill go tomicro-taskPush the microtask to the main thread. There is an important difference between the execution of the microtask and the execution of the macro task: the macro task is executed one by one, while the microtask is executed in teams. In other words, to perform a macro task, you need to perform a team of microtasks. (Note: It is still possible for new microtasks to be inserted during the execution of a microtaskmicro-taskSo in this case,Event LoopStill need to be this timeTick(Loop) The microtask is executed in the main thread.

  • The browser performs rendering operations to update the interface
  • Check for presenceWeb workerTasks, if any, are processed.
  • The process repeats until both queues are empty

Browser render timing

In the execution mechanism of the browser Event Loop above, there is a very important piece of content, which is the browser’s rendering time. The browser will wait until the current micro-task is empty, and then re-render. So if you need to re-render the DOM after an asynchronous operation the best way is to wrap it as a Micro task so that the DOM rendering will be done within this Tick.

Analysis of interview questions

Now that you know what the above interview question is about, we can use our understanding of the Event Loop to analyze the execution of this question:

// Print the order of log after executing the following code
console.log('script start')

// The "await" here is not easy to understand, I changed it to another way
function async1() {
  async2().then(res= > {
    console.log('async1 end')})}function async2() {
  console.log('async2 end')
  return Promise.resolve(undefined);
}
async1()

setTimeout(function() {
  console.log('setTimeout')},0)

new Promise(resolve= > {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')})console.log('script end')

// Log Print order: script start -> async2 end -> Promise -> script end -> asynC1 end -> promise1 -> promise2 -> setTimeout
Copy the code
  • The script hits console.log at the beginning of the script and prints script start
  • Parsing to async1(), the async1 execution environment is pushed onto the execution stack and the parsing engine enters Async1
  • It was found that async1 called async2 internally, so it continued to enter async2 and pushed the async2 execution environment onto the execution stack
  • When console.log is hit, async2 end is printed
  • Resolve (undefined). Since the Promise has changed to resolve, the async1 then registered callback is pushed into microTask
  • Parse to setTimeout, wait 0ms and push its callback into MacroTask
  • Continue execution until a Promise is reached. The internally registered callback of new Promise executes immediately, resolves inside the injection function, hits console.log, prints ‘Promise’, and then resolves to resolve, Push the callback function from the first THEN into micro-Task, and then push the callback function from the second THEN into micro-Task.
  • Execute to the last piece of code, print script end
  • Since then, one macro task in the first round of Tick is completed and the micro-task queue starts to execute. According to the previous analysis, there are three micro-tasks in the current micro-task, which are as follows: Console. log(‘async 1’), console.log(‘promise1’), console.log(‘promise2’) Async1, promise1, promise2
  • From there, the micro-Task is empty and the browser starts to re-render (if DOM manipulation is involved)
  • The Event Loop starts a new Tick again, takes a (unique) macro task from the macro task queue, and prints: setTimeout

This article has been included in the front-end interview Guide column

Relevant reference

  1. Front-end performance optimization principles and practices
  2. The way of the front-end interview

Previous content recommended

  1. Thoroughly understand throttling and anti-shaking
  2. [Basic] Principles and applications of HTTP and TCP/IP protocols
  3. 【 practical 】 WebPack4 + EJS + Express takes you through a multi-page application project architecture