The stack and queue
To access data in computer memory, the basic data structure is divided into stacks and queues.
A Stack, “Stack,” is a lifO data structure. Note that a Stack is sometimes referred to as a “Stack,” but a “Stack” is another complex data structure that is completely different from a Stack. The characteristic of a stack is that operations are performed at one end. Generally speaking, there are only two types of operations on a stack: loading and unloading. The first data on the stack is always the last to come out.
A Queue is similar to a stack, but it is a first-in, first-out data structure, with inserts at one end of the Queue and deletions at the other.
A popular analogy is that a stack is like a standing bucket. The data put into the stack first will be placed at the bottom of the bucket, and the data will be taken out one by one at the mouth of the bucket when the stack is removed. Therefore, the data put into the stack first will always be taken out last. A queue is like a water pipe. The data that is put in the queue first comes out at the other end of the queue. This is the biggest difference.
In javascript, the execution of a function is a typical loading and unloading process:
function fun1() {
function fun2() {
function fun3() {
console.log('do it');
}
fun3();
}
fun2();
}
fun1();
Copy the code
During program execution, will first fun1, fun2, fun3 in turn into the stack, and when calling a function, is fun3 call stack (the) first, then the fun2 and fun1, imagine if fun1 in first out stacks, then function fun2 and fun3 will be lost.
Single threaded and asynchronous
Javascript is a language where programs are single-threaded, with only one main thread. Why is this? Because as you can imagine, javascript was originally designed to run as a scripting language in the browser, and if it was designed to be multithreaded, with two threads modifying the DOM at the same time, whose would that be? So javascript is single-threaded, and in a thread the code will go down line by line until the program runs out, and if there is a time-consuming operation in between, it will have to wait.
Single thread design makes the execution efficiency of language is very poor, to take advantage of multicore CPU performance, javascript language support asynchronous code, when have more time-consuming operation, the task can be written as asynchronous execution, when an asynchronous task has not been performed, the main thread will hang asynchronous tasks, continue to implement the back of the synchronization code, Then go back and execute it if an asynchronous task has finished.
This way of executing the code is actually very consistent with many scenes in our life. For example, Xiaoming came home from work and he was very thirsty and wanted to boil water for tea. If it is synchronous execution, it is to boil water. If it is done asynchronously, Xiao Ming will boil the water first and then go to do something else, such as watching TV and listening to music, and then make tea when the water boils. Obviously the second asynchronous approach is more efficient.
What are the common asynchronous operations? There are many, we can list a few common ones:
- Ajax
- DOM event operations
- setTimeout
- Promise’s then method
- Node read file
Let’s start with a piece of code:
/ / sample 1
console.log(1);
setTimeout(function () {
console.log(2);
}, 1000);
console.log(3);
Copy the code
This code is very simple and executes them in a browser as follows:
1 2 3Copy the code
Since the setTimeout function is executed 1000 milliseconds late, it is logical to print 1 and 3 first, and 2 1000 milliseconds later.
Let’s change the code slightly to set setTimeout to 0:
/ / sample 2
console.log(1);
setTimeout(function () {
console.log(2);
}, 0); //0 ms, no delay
console.log(3);
Copy the code
Running results:
1 2 3Copy the code
Why is there a delay of 0 ms or 2 in the last output? Before you worry, let’s look at another piece of code:
/ / sample 3
console.log(1);
setTimeout(function () {
console.log(2);
}, 0);
Promise.resolve().then(function(){
console.log(3);
});
console.log(4);
Copy the code
Running results:
4 3 2 1Copy the code
If you can write the result correctly and explain why you output it this way, then you have a good understanding of the javascript event loop. If not, let’s talk about what’s going on here. It’s actually quite interesting.
How does javascript work?
We started with a brief talk about the basic data structure. Does it have anything to do with what we now call event loops? Of course, the first thing to be clear about is that javascript code execution is all on the stack, whether it’s synchronous code or asynchronous code.
While code is roughly divided into synchronous and asynchronous code, asynchronous code can be further divided into two categories: macro tasks and micro tasks.
Regardless of what macro and micro tasks are, often this kind of lofty terminology is not good for our understanding, let’s think of it this way: macro, that is, macro, large; Micro means microscopic, small.
Javascript is an interpreted language, and it works like this:
- Interpret each JS statement from top to bottom
- If the task is synchronized, it is pushed onto a stack (main thread); If it is an asynchronous task, it is placed in a task queue
- Start performing synchronization tasks on the stack until all tasks on the stack are gone, at which point the stack is cleared
- Looking back at the asynchronous queue, if an asynchronous task has completed, an event is generated and a callback is registered, pushing it onto the stack
- Return to step 3 until the asynchronous queue is empty and the program is finished
The difficulty of language description is not as good as looking at pictures:
From the above steps, it can be seen that whether synchronous or asynchronous, as long as it is executed in the stack, and check the asynchronous queue again and again, this execution mode is called “event loop”.
After understanding the execution principle of javascript, it is not difficult for us to understand why the second code is executed last when setTimeout is 0, because setTimeout is asynchronous code and it must wait for all synchronous codes to be executed before the asynchronous queue is executed. Even if setTimeout executes fast, it cannot be executed before the code is synchronized.
Event rings in the browser
Asynchronous tasks are divided into micro tasks and macro tasks. What is the execution mechanism of asynchronous tasks?
Attention! Microtasks and macro tasks are executed differently in browsers and Nodes, differently! Let’s go over the important things a little bit, but let’s talk about the browser environment.
In a browser execution environment, small, micro tasks are always executed first, followed by large, macro tasks. Looking back at the third piece of code, why does Promise’s then method execute before setTimeout? The rationale is that because Promise’s then method is a microtask, setTimeout is a macro task.
Next, let’s use a picture of Teacher Ruan Yifeng to illustrate:
In fact, the above diagram can be further refined, this diagram shows only one asynchronous queue, that is, there is no distinction between micro task queue and macro task queue. We can imagine adding a microtask queue to the graph, adding a judgment when javascript executes, adding a microtask to the microtask queue, adding a macro task to the macro task queue, and the browser always emptying the microtask first when emptying the queue. That’s all it takes to remove the browser’s event loop.
Finally, a big test, what is the result of the following code:
<script type="text/javascript">
setTimeout(function () {
console.log(1);
Promise.resolve().then(function () {
console.log(2);
});
});
setTimeout(function () {
console.log(3);
});
Promise.resolve().then(function () {
console.log(4);
});
console.log(5);
</script>
Copy the code
Copy this code to Chrome and run it. The result is:
5, 4, 1, 2, 3Copy the code
Let’s try to analyze why this is the case, and print 5 first, because console.log(5) is synchronous code, which is nothing to say.
Then, put the first two setTimeout and the last Promise into the asynchronous queue, and pay attention to the distinction between them. At this time, after the synchronization code is executed, both the microtask and the macro task queue are found. According to the event loop mechanism of the browser, the microtask is preferentially executed.
Then execute the first setTimeout in the macro task queue, printing 1.
At this point, there’s another Promise in the setTimeout, which is put into the microtask queue.
Empty the microtask queue again, printing 2.
Finally, there is a final setTimeout in the macro task queue, which prints 3.
Event loops in Node
The Node event loop is a bit different from the browser. There is a diagram in the official documentation of Node.js that illustrates the event loop mechanism in detail.
As you can see, the event loop mechanism in Node.js is divided into six phases, the three most important of which are highlighted above:
- The Timer phase refers to macro tasks such as setTimeout
- Poll polling phase, macro tasks such as reading files
- Check phase, setImmediate Macro task
Each stage in the figure represents a macro task queue. In the Node event ring, microtasks are executed first, and they are executed between each macro task queue and the next one after each macro task queue has been emptied. This is the biggest difference from the browser.
Let’s go back to code. Here’s a classic Node.js event loop interview question:
const fs = require('fs');
fs.readFile('./1.txt', (err, data) => {
setTimeout((a)= > {
console.log('timeout');
});
setImmediate((a)= > {
console.log('immediate');
});
Promise.resolve().then((a)= > {
console.log('Promise');
});
});
Copy the code
Running results:
Promise
immediate
timeout
Copy the code
The code is not complicated, first a file is read using the FS module, and inside the callback there are two macro tasks and a micro task. The micro task is always better than the macro task, so output the Promise first.
But why output immdiate first? The reason for this is that the fs macro task that reads the file is in the fourth polling phase, and after the fourth phase clears the queue, it moves to the fifth check phase, where the setImmediate macro task does not jump back to the first phase, thus outwriting immedate.
The tail
In conclusion, analyzing the browser and Node event rings is not easy, but you can do it by remembering the differences between them.
The browser event loop empties the microtask queue immediately after running a macro task. The Node event loop empties the macro task queue for a phase and then empties the microtask queue.
Finally, to summarize the common macro and micro tasks:
Macro task | Micro tasks |
---|---|
setTimeout | Promise’s then method |
setInterval | process.nextTick |
setImmediate | MutationObserver |
MessageChannel |