Table of contents
In the last section, we talked about what an execution stack is. Everyone knows that when we execute JS code, we actually put functions into the execution stack. What should we do when we encounter asynchronous code? In fact, when encountering asynchronous code, it will be suspended and added to the Task (there are multiple tasks) queue when it needs to be executed. Once the execution stack is empty, the Event Loop will take the code that needs to be executed from the Task queue and put it into the execution stack for execution, so in essence, the asynchronous or synchronous behavior in JS.
Different task sources will be assigned to different Task queues. Task sources can be divided into microtasks and macrotasks. In the ES6 specification, microtask is called jobs and macrotask is called task. Let's look at the execution sequence of 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')
First, let's explain the execution order of async and await of the above code. When we call the async1 function, async2 end will be output immediately, and the function will return a Promise. Then when we encounter await, the thread will be allowed to execute the code outside of async1, so we can completely regard await as a yield The logo of the thread. Then when the synchronous code is all executed, it will execute all the asynchronous code, then it will return to the await position to execute the resolve function of the returned Promise, which will put the resolve into the micro task queue, and then execute it. The callback in then, when the callbacks in the two then are all executed, they will return to the await position to process the return value. At this time, you can regard it as Promise.resolve(return value).then(), and then after await The code is all wrapped into the callback of then, so console.log('async1 end') will be executed first in setTimeout. If you think the above explanation is still a bit convoluted, then I will transform these two functions of async into code that you can understand.
new Promise((resolve, reject) => {
console.log('async2 end')
// Promise.resolve() inserts the code into the end of the microtask queue
// resolve insert the end of the micro task queue again
resolve(Promise.resolve())
}).then(() => {
console.log('async1 end')
})
In other words, if await is followed by Promise, async1 end needs to wait three ticks before it can be executed. In fact, the performance is relatively slow, so the V8 team borrowed from a bug in Node 8 and reduced the three tick to two at the bottom of the engine. However, this practice is actually against the law and regulations. Of course, the regulations can also be changed. This is a PR of the V8 team. This practice has been approved.
So the execution sequence of Event Loop is as follows: -First execute the synchronization code, which is a macro task -When all the synchronous code is executed, the execution stack is empty, query whether there is asynchronous code to be executed -Perform all micro tasks When all micro tasks are executed, the page will be rendered if necessary -Then start the next round of Event Loop, execute the asynchronous code in the macro task, that is, the callback function in setTimeout
So although the above code setTimeout is written before Promise, because Promise is a micro task and setTimeout is a macro task, there will be the above printing. Micro tasks include process.nextTick, promise, and MutationObserver. Macro tasks include script, setTimeout, setInterval, setImmediate, I/O, UI rendering. Many people here have a misunderstanding, thinking that microtasks are faster than macrotasks is actually wrong. Because the script is included in the macro task, the browser will execute a macro task first, and then the micro task will be executed first if there is asynchronous code.
Event Loop in Node
The Event Loop in Node is completely different from the one in the browser. Node's Event Loop is divided into 6 stages, which will run repeatedly in order. Whenever entering a certain stage, the function will be taken out of the corresponding callback queue for execution. When the queue is empty or the number of executed callback functions reaches the threshold set by the system, it will enter the next stage.
timer The timers phase will execute the setTimeout and setInterval callbacks and is controlled by the poll phase. Similarly, the time specified by the timer in Node is not an accurate time, it can only be executed as soon as possible.
I/O The I/O phase will handle a few unexecuted I/O callbacks from the previous cycle idle, prepare The internal implementation of idle and prepare phases is ignored here.
poll Polling is a crucial stage, in this stage, the system will do two things Return to the timer stage to execute the callback Perform I/O callback
-And if the timer is not set when entering this stage, the following two things will happen -If the poll queue is not empty, it will traverse the callback queue and execute synchronously until the queue is empty or the system limit is reached
1.If the poll queue is empty, two things will happen 2.If there is a setImmediate callback to be executed, the poll phase will stop and enter the check phase to execute the callback 3.If there is no setImmediate callback to be executed, it will wait for the callback to be added to the queue and execute the callback immediately. There will also be a timeout setting to prevent waiting forever 4.Of course, if the timer is set and the poll queue is empty, it will determine whether there is a timer timeout, and if so, it will return to the timer stage to execute the callback.
check check phase execution setImmediate close callbacks The close callbacks phase executes the close event In the above content, we understand the execution order of the Event Loop in Node, and then we will use the code to understand this content in depth. First of all, in some cases, the execution order of the timer is actually random
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
For the above code, setTimeout may be executed before or after -First of all setTimeout(fn, 0) === setTimeout(fn, 1), which is determined by the source code -Entering the event loop is also costly. If more than 1ms is spent in preparation, then the setTimeout callback will be executed directly in the timer phase -So if the preparation time takes less than 1ms, then the setImmediate callback is executed first
Of course, in some cases, their order of execution must be fixed, such as the following code:
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
In the above code, setImmediate is always executed first. Because the two codes are written in the IO callback, the IO callback is executed in the poll phase. After the callback is executed, the queue is empty and the setImmediate callback is found, so it jumps directly to the check phase to execute the callback.