banner
SlhwSR

SlhwSR

热爱技术的一名全栈开发者
github
bilibili

Event Loop Principle

1. What is it#

First, JavaScript is a single-threaded language, which means that only one thing can be done at a time. However, this does not mean that single-threading is blocking. The method to achieve single-threaded non-blocking is through the event loop.

In JavaScript, all tasks can be divided into:

  • Synchronous tasks: tasks that are executed immediately. Synchronous tasks generally go directly to the main thread for execution.

  • Asynchronous tasks: tasks that are executed asynchronously, such as ajax network requests, setTimeout timing functions, etc.

The flowchart of synchronous tasks and asynchronous tasks is as follows:

image

From the above, we can see that synchronous tasks enter the main thread, which is the main execution stack, and asynchronous tasks enter the task queue. When the tasks in the main thread are executed and empty, the corresponding tasks in the task queue will be read and pushed into the main thread for execution. This process is repeated continuously, which is the event loop.

2. Macro Tasks and Micro Tasks#

If tasks are divided into synchronous tasks and asynchronous tasks, it is not so accurate. Let's take an example:

console.log(1)

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

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

console.log(3)

If we analyze the code according to the flowchart above, we will get the following execution steps:

  • console.log(1): synchronous task, executed in the main thread
  • setTimeout(): asynchronous task, put into the Event Table, the callback console.log(2) is pushed into the Event Queue after 0 milliseconds
  • new Promise: synchronous task, executed directly in the main thread
  • .then: asynchronous task, put into the Event Table
  • console.log(3): synchronous task, executed in the main thread

So according to the analysis, the result should be 1 => 'new Promise' => 3 => 2 => 'then'

But the actual result is: 1 => 'new Promise' => 3 => 'then' => 2

The reason for the discrepancy lies in the execution order of asynchronous tasks. The event queue is actually a "first in, first out" data structure, and events that are in front will be read by the main thread first.

In the example, the callback event of setTimeout enters the queue first and should be executed before the one in .then, but the result is the opposite.

The reason is that asynchronous tasks can be further divided into micro tasks and macro tasks.

Micro Tasks#

A function that needs to be executed asynchronously, and its execution time is after the main function is executed and before the current macro task is completed.

Common micro tasks include:

  • Promise.then

  • MutationObserver

  • Object.observe (deprecated; replaced by Proxy objects)

  • process.nextTick (Node.js)

Macro Tasks#

Macro tasks have a larger time granularity and the execution interval cannot be precisely controlled, which is not suitable for some high real-time requirements.

Common macro tasks include:

  • script (can be understood as outer synchronous code)
  • setTimeout/setInterval
  • UI rendering/UI events
  • postMessage, MessageChannel
  • setImmediate, I/O (Node.js)

At this time, the relationship between the event loop, macro tasks, and micro tasks is shown in the following diagram:

image

According to this process, the execution mechanism is as follows:

  • Execute a macro task. If there are micro tasks encountered, put them into the micro task event queue.
  • After the current macro task is completed, check the micro task event queue, and then execute all the micro tasks inside.

Going back to the previous question:

console.log(1)
setTimeout(()=>{
    console.log(2)
}, 0)
new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})
console.log(3)

The flow is as follows:

// Encountered console.log(1), directly print 1
// Encountered the timer, which is a new macro task, leave it for later execution
// Encountered new Promise, this is executed directly, print 'new Promise'
// .then is a micro task, put it into the micro task queue, execute it later
// Encountered console.log(3), directly print 3
// Okay, this round of macro task is completed, now check if there are any micro tasks in the micro task list. Found the callback of .then, execute it, print 'then'
// When a macro task is completed, go to execute a new macro task. Now there is only one timer macro task left, execute it, print 2

3. async and await#

async means asynchronous, and await can be understood as async wait. So async is used to declare an asynchronous method, and await is used to wait for the execution of an asynchronous method.

async#

The async function returns a promise object. The following two methods are equivalent:

function f() {
    return Promise.resolve('TEST');
}

// asyncF is equivalent to f!
async function asyncF() {
    return 'TEST';
}

await#

Under normal circumstances, the await command is followed by a Promise object, and it returns the result of the object. If it is not a Promise object, it directly returns the corresponding value.

async function f(){
    // Equivalent to
    // return 123
    return await 123
}
f().then(v => console.log(v)) // 123

No matter what follows await, it will block the code below.

async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) // Blocked
}

async function fn2 (){
    console.log('fn2')
}

fn1()
console.log(3)

In the example above, await will block the code below (i.e., add it to the micro task queue), execute the synchronous code outside the async function first, and then go back to the async function to execute the blocked code.

So the output of the above example is: 1, fn2, 3, 2

4. Flow Analysis#

Through the above understanding, we have a general understanding of the execution order of JavaScript in various scenarios.

Here is the code directly:

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('settimeout')
})
async1()
new Promise(function (resolve) {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')

Analysis process:

  1. Execute the entire code. When encountering console.log('script start'), directly print the result, output script start.
  2. Encounter a timer, which is a macro task, so it is not executed first.
  3. Encounter async1(), execute the async1 function, first print async1 start, then encounter await, what to do? Execute async2(), print async2, and then block the code below (i.e., add it to the micro task list), jump out to execute the synchronous code.
  4. Jump to new Promise, execute it directly, print promise1, then encounter .then(), which is a micro task, put it into the micro task list and wait for execution.
  5. The last line directly prints script end, now the synchronous code is executed, start executing the micro tasks, that is, the code below await, print async1 end.
  6. Continue to execute the next micro task, that is, execute the callback of .then(), print promise2.
  7. All the tasks of the previous macro task are completed, start the next macro task, which is the timer, print settimeout.

So the final result is: script start, async1 start, async2, promise1, script end, async1 end, promise2, settimeout.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.