Explain the Event Loop mechanism in JavaScript in detail

foreword

We all know that javascript has been a single-threaded non-blocking scripting language since its birth. This is determined by its original purpose: interacting with the browser.

Single-threaded means that at any time the JavaScript code is executing, there is only one main thread that handles all the tasks.

Non-blocking is when the code needs to perform an asynchronous task (a task that cannot return the result immediately and takes a certain amount of time to return, such as an I/O event), the main thread will suspend the task, and then in When the asynchronous task returns the result, the corresponding callback is executed according to certain rules.

Single threading is necessary and the cornerstone of the javascript language. One of the reasons is that in its original and most important execution environment, the browser, we need to perform various dom operations. Imagine if javascript is multi-threaded, then when two threads perform an operation on the dom at the same time, for example, one adds an event to it, and the other deletes the dom, how to deal with it at this time? Therefore, in order to ensure that a situation similar to this example does not occur, javascript chooses to use only one main thread to execute the code, thus ensuring the consistency of program execution.

Of course, nowadays people also realize that single thread not only guarantees the execution order but also limits the efficiency of javascript, so the web worker technology is developed. This technology claims to make javascript a multithreaded language.

However, multi-threading using web worker technology has many limitations, for example: all new threads are fully controlled by the main thread and cannot be executed independently. This means that these "threads" should actually be children of the main thread. In addition, these child threads do not have permission to perform I/O operations, and can only share some tasks such as computations with the main thread. So strictly speaking, these threads do not have complete functions, so this technology does not change the single-threaded nature of the javascript language.

It is foreseeable that javascript will always be a single-threaded language in the future.

Having said that, another feature of javascript mentioned earlier is "non-blocking", so how does the javascript engine achieve this? The answer is the protagonist of today's article - event loop (event loop).

Note: Although there is also an event loop in nodejs that is similar to that in a traditional browser environment. However, there are many differences between the two, so they are separated and explained separately.

text

Event loop mechanism of js engine in browser environment

1. Execution stack and event queue

When javascript code is executed, different variables are stored in different locations in memory: the heap and the stack to distinguish them. Among them, some objects are stored in the heap. The stack stores some basic type variables and pointers to objects. But the meaning of the execution stack we are talking about here is somewhat different from the above stack.

We know that when we call a method, js will generate an execution environment (context) corresponding to this method, also called the execution context. In this execution environment, there is the private scope of the method, the pointer to the upper scope, the parameters of the method, the variables defined in this scope, and the this object of this scope. When a series of methods are called in sequence, because js is single-threaded, only one method can be executed at the same time, so these methods are queued in a separate place. This place is called the execution stack.

When a script is executed for the first time, the js engine will parse this code, add the synchronized code in it to the execution stack in the order of execution, and then execute it from the beginning. If a method is currently being executed, then js will add the execution environment of this method to the execution stack, and then enter this execution environment to continue executing the code in it. When the code in the execution environment is executed and the result is returned, js will exit the execution environment and destroy the execution environment, returning to the execution environment of the previous method. . This process is repeated until all the code in the execution stack has been executed.

The following picture shows this process very intuitively, where global is the code added to the execution stack when the script is first run:

 

 

 

 

As can be seen from the picture, the execution of a method will add the execution environment of the method to the execution stack. In this execution environment, other methods can also be called, or even itself. The result is just to add another execution environment to the execution stack. This process can go on indefinitely, unless a stack overflow occurs, that is, the maximum amount of memory that can be used is exceeded.

The above process is all about the execution of synchronous code. So what happens when an asynchronous code (such as sending ajax request data) is executed? As mentioned earlier, another major feature of js is non-blocking. The key to achieving this lies in the mechanism to be mentioned below - the event queue (Task Queue).

After the js engine encounters an asynchronous event, it will not wait for its return result, but will suspend the event and continue to execute other tasks in the execution stack. When an asynchronous event returns the result, js will add the event to another queue different from the current execution stack, which we call the event queue. When it is put into the event queue, its callback will not be executed immediately, but it will wait for all tasks in the current execution stack to be executed. When the main thread is in an idle state, the main thread will check whether there are tasks in the event queue. If there is, then the main thread will take out the event in the first place, and put the callback corresponding to this event into the execution stack, and then execute the synchronization code in it... and so on, thus forming an infinite cycle. That's why this process is called the "Event Loop".

Here's another diagram to illustrate the process:

 

 

The stack in the figure represents what we call the execution stack, the web apis represent some asynchronous events, and the callback queue is the event queue.

2.macro task与micro task

The above event loop process is a macro expression. In fact, because asynchronous tasks are not the same, their execution priorities are also different. Different asynchronous tasks are divided into two categories: micro tasks and macro tasks.

The following events are macro tasks:

  • setInterval()
  • setTimeout()

The following events are microtasks

  • new Promise()
  • new MutaionObserver()

As we mentioned earlier, in an event loop, asynchronous events will be placed in a task queue after returning the result. However, depending on the type of asynchronous event, the event will actually go to the corresponding macrotask queue or microtask queue. And when the current execution stack is empty, the main thread will check whether there is an event in the microtask queue. If it does not exist, then go to the macro task queue to take out an event and add the corresponding back to the current execution stack; if it exists, the callback corresponding to the event in the queue will be executed in turn until the micro task queue is empty, and then go to the macro task The first event is taken out of the queue, and the corresponding callback is added to the current execution stack... This is repeated, and the loop is entered.

We just need to remember that when the current execution stack is completed, all events in the microtask queue will be processed immediately, and then an event will be taken out of the macrotask queue. In the same event loop, microtasks are always executed before macrotasks.

This explains the result of the following code:

setTimeout(function () {
    console.log(1);
});

new Promise(function(resolve,reject){
    console.log(2)
    resolve(3)
}).then(function(val){
    console.log(val);
})

The result is:

2
3
1
 

Event loop mechanism in node environment

1. How is it different from the browser environment?

In node, the event loop exhibits much the same state as it does in the browser. The difference is that node has its own set of models. The implementation of the event loop in node relies on the libuv engine. We know that node selects the chrome v8 engine as the js interpreter. The v8 engine analyzes the js code to call the corresponding node api, and these apis are finally driven by the libuv engine to perform corresponding tasks and place different events in different The queue is waiting for the main thread to execute. So actually the event loop in node exists in the libuv engine.

2. Event Loop Model

Here is a model of the event loop in the libuv engine:

 ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

Note: Each block in the model represents a stage of the event loop

This model is given in an article on node's official website, and my explanation below is also derived from this article. I will post the address of the article at the end of the article, and friends who are interested can check the original text in person.

3. Detailed explanation of each stage of the event loop

From the above model, we can roughly analyze the order of the event loop in node:

External input data --> polling stage (poll) --> check stage (check) --> close event callback stage (close callback) --> timer detection stage (timer) --> I/O event callback stage (I/O callbacks)-->idle stage (idle, prepare)-->polling stage...

The names of the above stages are translations based on my personal understanding. In order to avoid errors and ambiguities, these stages will be expressed in English in the following explanations.

The approximate functions of these stages are as follows:

  • timers: This phase executes callbacks such as setTimeout()and setInterval().
  • I/O callbacks: This phase executes almost all callbacks. But does not include close events, timers and setImmediate()callbacks.
  • idle, prepare: This stage is only used internally and can be ignored.
  • poll: Waiting for new I/O events, node blocks here in some special cases.
  • setImmediate()The callback of check: will be executed in this phase.
  • close callbacks: such as the callbacks for socket.on('close', ...)this close event.

Let's explain these stages in detail in the order in which the code first enters the libuv engine:

poll stage

When a v8 engine parses the js code and passes it to the libuv engine, the loop first enters the poll stage. The execution logic of the poll phase is as follows: First, check whether there are events in the poll queue, and execute the callbacks in the order of first-in, first-out if there are tasks. When the queue is empty, it will check whether there is a callback of setImmediate(), and if so, it will enter the check phase to execute these callbacks. But at the same time, it will also check whether there are expired timers. If so, put the callbacks of these expired timers into the timer queue in the calling order, and then the loop will enter the timer phase to execute the callbacks in the queue. The order of the two is not fixed and is affected by the environment in which the code runs. If both queues are empty, the loop will stay in the poll stage until an i/o event returns, the loop will enter the i/o callback stage and execute the callback of this event immediately.

It is worth noting that the poll phase does not actually execute infinitely when executing callbacks in the poll queue. There are two situations in which the poll phase will terminate the execution of the next callback in the poll queue: 1. All callbacks are executed. 2. The number of executions exceeds the limit of the node.

check stage

The check phase is specially used to execute setImmediate()the callback of the method. When the poll phase enters the idle state and there is a callback in the setImmediate queue, the event loop enters this phase.

close stage

When a socket connection or a handle is abruptly closed (for example, by calling a socket.destroy()method), the close event is sent to this stage to execute the callback. Otherwise the event will be process.nextTick()sent out with the method.

timer stage

This stage executes the callback of all expired timers added to the timer queue in a first-in, first-out manner. A timer callback refers to a callback function set by the setTimeout or setInterval function.

I/O callback stage

As mentioned above, this phase mainly executes callbacks for most I/O events, including some for the operating system. For example, when an error occurs in a TCP connection, the system needs to execute a callback to get a report of the error.

4. The difference and usage scenarios of process.nextTick, setTimeout and setImmediate

There are three commonly used methods for delaying task execution in node: process.nextTick, setTimeout (same as setInterval) and setImmediate

There are some very different differences between the three:

process.nextTick()

Although not mentioned, there is actually a special queue in node, the nextTick queue. Although the callback execution in this queue is not represented as a stage, these events will be executed first when each stage is completed and ready to enter the next stage. When the event loop is ready to enter the next stage, it will first check whether there are tasks in the nextTick queue, and if so, it will first empty the queue. Unlike executing tasks in the poll queue, this operation does not stop until the queue is emptied. This also means that incorrect usage process.nextTick()will cause node to enter an infinite loop. . until memory leaks.

So is it more appropriate to use this method? Here's an example:

const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});

In this example, when the listen method is called, unless the port is occupied, it will immediately bind to the corresponding port. This means that this port can immediately fire the listening event and execute its callback. However, at this time, on('listening)the callback has not been set, and naturally no callback can be executed. To avoid this, node uses a process.nextTick()method on the listen event to ensure that the event is fired after the callback function is bound.

setTimeout()和setImmediate()

Of the three methods, these two are the most easily confused. In fact, the two methods also perform very similarly in some cases. In practice, however, the meanings of the two methods are quite different.

setTimeout()The method is to define a callback, and hope that this callback will be executed the first time after the time interval we specify. Note that this "execute the first time" means that, due to the many influences of the operating system and the currently executing task, the callback will not be executed exactly after the time interval we expect. There is a certain delay and error in the execution time, which is inevitable. Node will execute the task you set as soon as the timer callback can be executed.

setImmediate()The method will be executed immediately in the sense, but in fact it will execute the callback in a fixed phase, that is, after the poll phase. Interestingly, the meaning of the name process.nextTick()matches the method mentioned earlier. The developers of node are also aware that there is a certain confusion in the naming of these two methods, and they said that they will not exchange the names of these two methods---because a large number of node programs use these two methods, and the names are exchanged. The benefits are nothing compared to its impact.

setTimeout()setImmediate()The performance is very similar to that without setting the time interval . Guess what is the result of the following code?

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

setImmediate(() => {
    console.log('immediate');
});

Actually, the answer is not necessarily. That's right, even the developers of node can't accurately judge the order of the two. It depends on the environment in which this code runs. Various complex situations in the runtime environment can cause the order of the two methods in the synchronization queue to be randomly determined. However, there is one case where the execution order of two method callbacks can be accurately determined, and that is in the callback of an I/O event. The order of the following code is always fixed:

const fs = require('fs');

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    setImmediate(() => {
        console.log('immediate');
    });
});

The answer is always:

immediate
timeout

Because in the callback of the I/O event, the callback of the setImmediate method is always executed before the callback of the timer.

end

The event loop of javascript is a very important and fundamental concept in the language. A clear understanding of the execution order of the event loop and the characteristics of each stage can give us a clear understanding of the execution order of a piece of asynchronous code, thereby reducing the uncertainty of code running. Reasonable use of various methods of delaying events will help the code to better execute according to its priority. This article expects to accurately describe the complex process of the event loop in the most understandable way and language, but due to the limited level of the author, omissions are inevitable in the article. If you find some problems in the article, you are welcome to put them in the comments, and I will try my best to reply to these comments and correct the mistakes.

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325020388&siteId=291194637