1. Why is JavaScript single-threaded?
A major feature of the JavaScript language is single threading, which means that you can only do one thing at the same time. So, why can't JavaScript have multiple threads? This will improve efficiency.
JavaScript's single threading is related to its purpose. As a browser scripting language, JavaScript's main purpose is to interact with users and operate DOM. This determines that it can only be single threaded, otherwise it will cause very complex synchronization problems. For example, suppose JavaScript has two threads at the same time, one thread adds content on a certain DOM node, and the other thread deletes this node, which thread should the browser take at this time?
Therefore, in order to avoid complexity, JavaScript is a single thread from its birth, which has become the core feature of this language and will not change in the future.
In order to utilize the computing power of multi-core CPUs, HTML5 proposed the Web Worker standard, allowing JavaScript scripts to create multiple threads, but the child threads are completely controlled by the main thread and cannot operate the DOM. Therefore, this new standard does not change the nature of JavaScript single threading.
2. Task queue
Single threading means that all tasks need to be queued, and the previous task will be executed before the next task will be executed. If the previous task takes a long time, the next task has to wait.
If the queue is because of the large amount of computing and the CPU is too busy, it would be fine, but many times the CPU is idle because the IO device (input and output device) is very slow (such as Ajax operation reads data from the network), and you have to wait for the result to come out before executing it.
The designer of the JavaScript language realized that at this time, the CPU could completely ignore the IO device, suspend the waiting tasks and run the next tasks first. Wait until the IO device returns the result, then turn around and continue the suspended task.
Therefore, JavaScript has two execution methods: one is that the CPU executes in sequence, the previous task ends, and then the next task is executed, which is called synchronous execution; the other is that the CPU skips tasks with a long wait time and processes the subsequent tasks first, which is called asynchronous execution. Programmers choose independently what kind of execution method to adopt.
Specifically, the operating mechanism of asynchronous execution is as follows. (Same is true for synchronous execution, as it can be considered as asynchronous execution without asynchronous tasks.)
(1) All tasks are executed on the main thread to form an execution context stack.
(2) In addition to the main thread, there is also a "task queue". The system places the asynchronous tasks into the "task queue" and then continues to execute subsequent tasks.
(3) Once all tasks in the "Execution Stack" are executed, the system will read the "task queue". If at this time, the asynchronous task has ended the waiting state, it will enter the execution stack from the "task queue" and resume execution.
(4) The main thread keeps repeating the third step above.
The following figure 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". This is the running mechanism of JavaScript. This process will be repeated continuously.
3. Events and callback functions
"Task Queue" is essentially a queue of events (also understood as a queue of messages). When an IO device completes a task, it adds an event to the "Task Queue", indicating that the relevant asynchronous tasks can enter the "execution stack". The main thread reads the "task queue", which means reading what events are inside.
Events in the "task queue" include events in addition to events from IO devices, but also events generated by users (such as mouse clicks, page scrolling, etc.). As long as the callback function is specified, these events will enter the "task queue" when they occur and wait for the main thread to read.
The so-called "callback" is the code that will be hung by the main thread. Asynchronous tasks must specify a callback function. When the asynchronous task returns from the "task queue" to the execution stack, the callback function will be executed.
"Task Queue" is a first-in-first-out data structure, with events that are ranked first and are preferred to return to the main thread. The main thread's reading process is basically automatic. As long as the execution stack is cleared, the first event on the "task queue" will automatically return to the main thread. However, due to the "timer" function mentioned later, the main thread needs to check the execution time, and some events must return to the main thread at the specified time.
4. Event Loop
The main thread reads events from the "task queue". This process is looping continuously, so the entire running mechanism is also called Event Loop.
To better understand Event Loop, please see the picture below (quoted from Philip Roberts's speech "Help, I'm stuck in an event-loop").
In the figure above, when the main thread is running, it generates heap and stack. The code in the stack calls various external APIs, which 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.
Execute the code in the stack, always executed before reading the "task queue". Please see the following example.
The code copy is as follows:
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 in the current script is executed. So, it is equivalent to the following writing method.
The code copy is as follows:
var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};
req.onerror = function (){};
That is to say, the parts of the specified callback function (onload and onerror) are not important before or after the send() method, because they are part of the execution stack, and the system will always execute them before reading the "task queue".
5. Timer
In addition to placing asynchronous tasks, the "task queue" also has another function, which is to place timed events, that is, specify how long certain code will be executed after. This is called the "timer" function, which is the code executed regularly.
The timer function is mainly completed by two functions: setTimeout() and setInterval(). Their internal running mechanisms are exactly the same. The difference is that the code specified by the former is executed at one time, while 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 postpone execution.
The code copy is as follows:
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 second line until after 1000 milliseconds.
If the second parameter of setTimeout() is set to 0, it means that the specified callback function (0 millisecond interval) is executed immediately after the current code has been executed (execution stack is cleared).
The code copy is as follows:
setTimeout(function(){console.log(1);}, 0);
console.log(2);
The execution results of the above code are always 2 and 1, because the system will execute the callback function in the "task queue" only after the second line is executed.
The HTML5 standard specifies that the minimum value (shortest interval) of the second parameter of setTimeout() must not be less than 4 milliseconds. If it is lower than this value, it will automatically increase. Before this, older browsers set the minimum interval to 10 milliseconds.
Additionally, for those DOM changes (especially the parts involving page re-rendering), 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() just inserts the event into the "task queue". You must wait until the current code (execution stack) is executed before the main thread will execute the callback function it specifies. If the current code takes a long time, it may take a long time to wait, so there is no guarantee that the callback function will be executed at the time specified by setTimeout().
6. Node.js Event Loop
Node.js is also a single-threaded Event Loop, but its running mechanism is different from that of the browser environment.
Please see the diagram below (author @BusyRich).
According to the above figure, the running mechanism of Node.js is as follows.
(1) 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, forms an Event Loop, and returns the execution results of the task to the V8 engine in an asynchronous manner.
(4) The V8 engine returns the result to the user.
In addition to the two methods setTimeout and setInterval, Node.js also provides two other methods related to "task queue": process.nextTick and setImmediate. They can help us deepen our understanding of "task queues".
The process.nextTick method can trigger the callback function at the end of the current "execution stack" before the main thread reads the "task queue" next time. That is, the tasks it specifies always occur before all asynchronous tasks. The setImmediate method triggers the callback function at the tail of the current "task queue", that is, the task it specifies is always executed the next time the main thread reads the "task queue", which is very similar to setTimeout(fn, 0). Please see the following example (via StackOverflow).
The code copy is as follows:
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, since the callback function specified by the process.nextTick method is always triggered at the tail of the current "execution stack", not only function A is executed first than the callback function timeout specified by setTimeout, but function B is also executed first than timeout. This means that if there are multiple process.nextTick statements (regardless of whether they are nested or not), they will all be executed on the current "execution stack".
Now, let's look at setImmediate.
The code copy is as follows:
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, there are two setImmediates. The first setImmediate specifies that the callback function A is triggered at the tail of the current "task queue" (the next "event loop"); then, setTimeout also specifies that the callback function timeout is triggered at the tail of the current "task queue", so in the output result, TIMEOUT FIRED is ranked behind 1. As for the 2 ranking behind TIMEOUT FIRED, it is because another important feature of setImmediate: an "event loop" can only trigger a callback function specified by setImmediate.
We have obtained an important difference from this: multiple process.nextTick statements are always executed at once, while multiple setImmediates require multiple times to be executed. In fact, this is exactly why Node.js version 10.0 adds 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!
The code copy is as follows:
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 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 is also more efficient in execution (because there is no need to check the "task queue").