Detailed explanation of JavaScript operating mechanism: Talk about Event Loop again
A year ago, I wrote an article “What is Event Loop?” ” , talked about my understanding of Event Loop.
Last month, I happened to see Philip Roberts’s speech “Help, I’m stuck in an event-loop” . Only then did I find embarrassingly that my understanding was wrong. I decided to rewrite this topic and describe the internal operating mechanism of the JavaScript engine in detail, completely and correctly. Below is my rewrite.
Before entering the text, insert a message. My new book “Introduction to ECMAScript 6” has been published ( copyright page , inner page 1 , inner page 2 ), printed in full color on coated paper, very beautiful, and attached with an index (of course the price is a little more expensive than similar books). Click here for preview and purchase .
( Updated on October 13, 2014 : This article has been revised to reflect my current understanding. For more explanations and examples of setTimeout, please refer to the “JavaScript Standard Reference Tutorial” I am writing .)
( Updated on October 11, 2014 : Teacher Ling Pu made a comment on this article , and pointed out the wrong statement in the article in detail. It is recommended to read it.)
1. Why is JavaScript single-threaded?
A major feature of the JavaScript language is single-threaded, that is, only one thing can be done at the same time. So, why can’t JavaScript have multiple threads? This can improve efficiency.
The single thread of JavaScript is related to its purpose. As a browser scripting language, the main purpose of JavaScript is to interact with users and manipulate the DOM. This determines that it can only be single-threaded, otherwise it will bring very complicated synchronization problems. For example, suppose that JavaScript has two threads at the same time. One thread adds content to a certain DOM node, and the other thread deletes this node. At this time, which thread should the browser use?
Therefore, in order to avoid complexity, JavaScript has been single-threaded since its birth. This has become a core feature of the language and will not change in the future.
In order to utilize the computing power of multi-core CPUs, HTML5 proposes the Web Worker standard, which allows JavaScript scripts to create multiple threads, but the child threads are completely controlled by the main thread and must not operate the DOM. Therefore, this new standard does not change the nature of JavaScript single-threaded.
Two, task queue
Single thread means that all tasks need to be queued, and the previous task is finished before the next task will be executed. If the previous task takes a long time, the latter task will have to wait forever.
If the queue is due to a large amount of calculations and the CPU is too busy, that’s fine, but in many cases the CPU is idle because the IO devices (input and output devices) are very slow (for example, Ajax operations read data from the network) and have to Wait for the results to come out, and then proceed.
The designers of the JavaScript language realized that at this time, the main thread can completely ignore the IO device, suspend the waiting tasks, and run the tasks that are ranked later. Wait until the IO device returns the result, then go back and continue the execution of the suspended task.
Therefore, all tasks can be divided into two types, one is synchronous tasks (synchronous), and the other is asynchronous tasks (asynchronous). Synchronous tasks refer to tasks that are queued for execution on the main thread, and only the previous task can be executed before the next task can be executed; asynchronous tasks refer to tasks that do not enter the main thread but enter the “task queue” (task queue) For tasks, only when the “task queue” informs the main thread that an asynchronous task can be executed, the task will enter the main thread for execution.
Specifically, the operating mechanism of asynchronous execution is as follows. (The same is true for synchronous execution, because it can be viewed as asynchronous execution without asynchronous tasks.)
(1) All synchronization tasks are performed on the main thread to form a execution stack (execution context stack).
(2) In addition to the main thread, there is also a “task queue”. As long as the asynchronous task has a running result, an event is placed in the “task queue”.
(3) Once all synchronization tasks in the “execution stack” are executed, the system will read the “task queue” to see what events are in it. Those corresponding asynchronous tasks then end the waiting state, enter the execution stack, and begin execution.
(4) The main thread keeps repeating the third step above.
The figure below is a schematic diagram of the main thread and task queue.
As long as the main thread is empty, it will read the “task queue”, which is the operating mechanism of JavaScript. This process will continue to repeat.
Three, events and callback functions
“Task queue” is a queue of events (can also be understood as a queue of messages). When an IO device completes a task, an event is added to the “task queue” to indicate that the related asynchronous task can enter the “execution stack”. The main thread reads the “task queue”, which is to read what events are in it.
In addition to the events of the IO device, the events in the “task queue” also include some user-generated events (such as mouse clicks, page scrolling, etc.). As long as the callback function is specified, these events will enter the “task queue” and wait for the main thread to read.
The so-called “callback” (callback) is the code that will be hung up by the main thread. Asynchronous tasks must specify a callback function. When the main thread starts to execute the asynchronous task, the corresponding callback function is executed.
“Task Queue” is a first-in-first-out data structure, and the first events are read by the main thread first. The reading process of the main thread is basically automatic. As long as the execution stack is emptied, the first event on the “task queue” will automatically enter the main thread. However, due to the “timer” function mentioned later, the main thread must first check the execution time. Certain events can only return to the main thread after the specified time.
Four, event loop
The main thread reads events from the “task queue”. This process is cyclical, so the entire operating mechanism is also called Event Loop.
In order to better understand the Event Loop, please see the picture below (quoted from Philip Roberts’ speech “Help, I’m stuck in an event-loop” ).
In the above figure, when the main thread is running, a heap and a stack are generated. The code in the stack calls various external APIs, and they add various events (click, load, done) to the “task queue”. As long as the code in the stack is executed, the main thread will read the “task queue” and execute the callback functions corresponding to those events in turn.
The code in the execution stack (synchronous task) is always executed before reading the “task queue” (asynchronous task). Please see the example below.
var req = new XMLHttpRequest(); req.open('GET', url); req.onload = function (){}; req.onerror = function (){}; req.send();
The req.send method in the above code is an Ajax operation to send data to the server. It is an asynchronous task, which means that the system will read the “task queue” only after all the code of the current script is executed. Therefore, it is equivalent to the following writing.
var req = new XMLHttpRequest(); req.open('GET', url); req.send(); req.onload = function (){}; req.onerror = function (){};
In other words, the part of the specified callback function (onload and onerror) does not matter before or after the send() method, because they are part of the execution stack, and the system always executes them before reading the “task queue” “.
Five, timer
In addition to placing events for asynchronous tasks, the “task queue” can also place timed events, that is, specify the time after which certain codes will be executed. This is called the “timer” function, which is the code that is executed regularly.
The timer function is mainly completed by the two functions setTimeout() and setInterval(). Their internal operating mechanisms are exactly the same. The difference is that the code specified by the former is executed once, and the latter is executed repeatedly. The following mainly discusses setTimeout().
setTimeout() accepts two parameters, the first is the callback function, and the second is the number of milliseconds to delay execution.
console.log(1); setTimeout(function(){console.log(2);},1000); console.log(3);
The execution result of the above code is 1, 3, 2, because setTimeout() delays the execution of the second line until after 1000 milliseconds.
If the second parameter of setTimeout() is set to 0, it means that after the current code is executed (the execution stack is cleared), the specified callback function will be executed immediately (0 millisecond interval).
setTimeout(function(){console.log(1);}, 0); console.log(2);
The execution result of the above code is always 2, 1, because only after the second line is executed, the system will execute the callback function in the “task queue”.
In short, the meaning of setTimeout(fn,0) is to specify a task to be executed in the earliest available free time of the main thread, that is, to execute as early as possible. It adds an event to the end of the “task queue”, so it will not be executed until the synchronization task and the existing events of the “task queue” are processed.
The HTML5 standard specifies the minimum value (shortest interval) of the second parameter of setTimeout(), which should not be less than 4 milliseconds. If it is lower than this value, it will automatically increase. Prior to this, older browsers set the minimum interval to 10 milliseconds. In addition, for those DOM changes (especially those that involve re-rendering of the page), they are usually not executed immediately, but every 16 milliseconds. At this time, the effect of using requestAnimationFrame() is better than setTimeout().
It should be noted that setTimeout() only inserts the event into the “task queue”. It must wait until the current code (execution stack) is executed before the main thread executes its designated callback function. If the current code takes a long time, it may take a long time, so there is no guarantee that the callback function will be executed at the time specified by setTimeout().
6. Event Loop of Node.js
Node.js is also a single-threaded Event Loop, but its operating mechanism is different from the browser environment.
Please see the diagram below (author @BusyRich ).
According to the above figure, the operating mechanism of Node.js is as follows.
(1) The V8 engine parses JavaScript scripts.
(2) The parsed code calls the Node API.
(3) The libuv library is responsible for the execution of the Node API. It assigns different tasks to different threads to form an Event Loop, which returns the execution results of the tasks to the V8 engine in an asynchronous manner.
(4) The V8 engine then returns the result to the user.
In addition to the two methods setTimeout and setInterval, Node.js also provides two other methods related to the “task queue”: process.nextTick and setImmediate . They can help us deepen our understanding of “task queue”.
The process.nextTick method can trigger the callback function at the end of the current “execution stack”-before the next Event Loop (the main thread reads the “task queue”). In other words, the task it specifies always occurs before all asynchronous tasks. The setImmediate method adds an event to the end of the current “task queue”, that is, the task it specifies is always executed in the next Event Loop, which is very similar to setTimeout(fn, 0). Please see the example below (via StackOverflow ).
process.nextTick(function A() { console.log(1); process.nextTick(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0) // 1 // 2 // TIMEOUT FIRED
In the above code, because the callback function specified by the process.nextTick method is always triggered at the end of the current “execution stack”, not only the function A is executed before the callback function timeout specified by setTimeout, but also the function B is executed before timeout. This means that if there are multiple process.nextTick statements (regardless of whether they are nested), they will all be executed on the current “execution stack”.
Now, look at setImmediate again.
setImmediate(function A() { console.log(1); setImmediate(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0);
In the above code, setImmediate and setTimeout(fn,0) each add a callback function A and timeout, both of which are triggered in the next Event Loop. So, which callback function is executed first? The answer is uncertain. The result of the operation may be 1–TIMEOUT FIRED–2, or TIMEOUT FIRED–1–2.
Confusingly, the Node.js documentation states that the callback function specified by setImmediate always comes before setTimeout. In fact, this happens only when calling recursively.
setImmediate(function (){ setImmediate(function A() { console.log(1); setImmediate(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0); }); // 1 // TIMEOUT FIRED // 2
In the above code, setImmediate and setTimeout are encapsulated in a setImmediate, its running result is always 1–TIMEOUT FIRED–2, then function A must be triggered before timeout. As for the second row behind TIMEOUT FIRED (that is, function B is triggered after timeout), it is because setImmediate always registers the event to the next round of Event Loop, so function A and timeout are executed in the same loop, and function B is in the next loop. Round Loop execution.
We have thus obtained an important difference between process.nextTick and setImmediate: multiple process.nextTick statements are always executed in the current “execution stack” at one time, and multiple setImmediate may require multiple loops to be executed. In fact, this is the reason why Node.js version 10.0 added the setImmediate method, otherwise the recursive call to process.nextTick like the following will be endless, and the main thread will not read the “event queue” at all!
process.nextTick(function foo() { process.nextTick(foo); });
In fact, now if you write a recursive process.nextTick, Node.js will throw a warning asking you to change it to setImmediate.
In addition, since the callback function specified by process.nextTick is triggered in this “event loop”, and setImmediate specifies that it is triggered in the next “event loop”, it is obvious that the former always happens earlier than the latter, and the execution efficiency Also high (because there is no need to check the “task queue”).
(over)