Single threaded model

The single-threaded model refers to JavaScript running on only one thread. That is, JavaScript can only perform one task at a time, and all other tasks must queue up later.

Note that just because JavaScript runs on a single thread does not mean that the JavaScript engine has only one thread. In fact, JavaScript engines have multiple threads, and a single script can only run on one thread (called the main thread), with the other threads cooperating in the background.

The reason JavaScript is single-threaded, rather than multi-threaded, is a matter of history. JavaScript has been single-threaded since its inception because it didn’t want to make the browser too complex, since multiple threads need to share resources and potentially modify each other’s results, which would be too complex for a web scripting language. If JavaScript has two threads at the same time, one thread adds content to the web DOM node, and the other thread removes the node, which thread should the browser use? Should there be a locking mechanism? So, to avoid complexity, JavaScript was originally single-threaded, which has become a core feature of the language and will not change in the future. Advantages and disadvantages of this mode:

  • The advantages are: the implementation is relatively simple, the execution environment is relatively simple;

  • The downside: As long as one task takes a long time, subsequent tasks must wait in line, delaying the execution of the entire program.

A common browser non-response (suspended animation) is usually the result of a single piece of JavaScript code running for so long (such as an infinite loop) that the entire page gets stuck in one place and no other task can be performed. The JavaScript language itself is not slow, but slow in reading and writing external data, such as waiting for Ajax requests to return results. At this point, if the server does not respond, or the network is not smooth, the script will be stalled for a long time.

If the queue is due to a large amount of computation, the CPU is too busy, but many times the CPU is idle because IO operations (input and output) are slow (such as Ajax operations reading data from the network) and have to wait for the results to come out before executing. The designers of the JavaScript language realized that the CPU could simply suspend the pending task and run the next one, regardless of the IO operation. Wait until the IO operation returns the result, then go back and continue the pending task. This mechanism is the “Event Loop” mechanism used in JavaScript.

The single-threaded model, while limiting JavaScript, gives it advantages that other languages don’t. When used well, JavaScript programs don’t clog, which is why Node can handle heavy traffic with very few resources.

In order to make use of the computing power of multi-core CPU, 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 cannot operate DOM. So, this new standard doesn’t change the single-threaded nature of JavaScript.

Browser multithreading

The browser is multi-process, each TAB TAB of the browser represents an independent process, among which the browser rendering process (browser kernel) belongs to a multi-process of the browser, mainly responsible for page rendering, script execution, event processing and so on. It includes: GUI rendering thread (responsible for rendering the page, parsing HTML, CSS DOM tree), JS engine thread, event trigger thread, timer trigger thread, HTTP request thread and other main threads.

JS is a single-threaded language, the browser only assigned to JS a main thread, used to perform a task (function), but can only perform one task at a time, the mission to form a task queue waiting to perform, but the front of certain tasks is very time-consuming, such as network request, timers and event listeners, if let them and other tasks, If you queue up to be executed honestly, the execution efficiency will be very low, and even lead to fake death of the page.

Browsers create additional threads for these time-consuming tasks, including HTTP request threads, browser timing triggers, and browser event triggers. These tasks are asynchronous.

Synchronous and asynchronous

All tasks in the program can be categorized into two categories: synchronous and asynchronous.

  • Synchronous tasks: Those tasks that are not suspended by the engine and are queued for execution on the main thread. You can perform the next task only after the previous task is completed.

  • Asynchronous tasks: Tasks that are put aside by the engine and not entered into the main thread but into the task queue. An asynchronous task (in the form of a callback function) is executed on the main thread only if the engine considers it ready to execute (such as an Ajax operation that results from the server). The code after the asynchronous task runs immediately without waiting for the asynchronous task to finish, which means that the asynchronous task has no “blocking” effect.

For example, Ajax operations can be handled as synchronous or asynchronous tasks, at the developer’s discretion. In the case of a synchronous task, the main thread waits for the Ajax operation to return the result and then executes. In the case of asynchronous tasks, the main thread makes an Ajax request and then executes the corresponding callback function after the Ajax operation results.

Task queues and event loops

In JavaScript runtime, in addition to a running main thread, the engine also provides a task queue of various asynchronous tasks that need to be handled by the current program. (In fact, there are multiple task queues depending on the type of asynchronous task. For ease of understanding, assume that there is only one queue.

First, the main thread performs all synchronization tasks. When all the synchronous tasks are completed, the asynchronous tasks in the task queue are viewed. If the condition is met, the asynchronous task is re-entered into the main thread to begin execution, and it becomes a synchronous task. The next asynchronous task enters the main thread to start execution. Once the task queue is empty, the program ends execution.

Asynchronous tasks are usually written as callback functions. Once the asynchronous task re-enters the main thread, the corresponding callback function is executed. If an asynchronous task does not have a callback function, it will not enter the task queue, that is, it will not re-enter the main thread, because there is no callback function to specify the next operation.

Main thread: that is, the main thread constantly reads events from the execution stack, and executes all synchronous code in the stack.

Task Queue: When an asynchronous event is encountered, instead of waiting for the asynchronous event to return the result, the event is suspended in a Queue separate from the execution stack, called a Task Queue.

How does a JavaScript engine know if an asynchronous task has a result, if it can get into the main thread? The answer is that the engine is constantly checking (observer mode), over and over again, to see if pending asynchronous tasks are ready to enter the main thread as soon as the synchronous task is finished. This Loop checking mechanism is called an Event Loop. Wikipedia defines it as: An event loop is a programming construct that waits for and dispatches events or messages in a program. There is also the concept of an execution stack.

Execution stack: When a function is executed, the user clicks the mouse once, Ajax is completed, an image is loaded, and other events occur, as long as the callback function is specified, these events will enter the execution stack queue, waiting for the main thread to read, following the first-in, first-out principle. To be clear, the main thread is a different concept from the execution stack. The main thread specifies which event in the execution stack is now executed.

The execution stack, the main thread and the task queue can be simply understood as follows: the task queue is different students, the main thread is the judge, the execution stack is the runway, and the main thread determines which task in the task queue is executed on the execution stack

As follows:

let a = () = > {
  setTimeout(() = > {
    console.log('Task queue function 1')},0)
  for (let i = 0; i < 5000; i++) {
    console.log('For loop of A')}console.log('A event completed')}let b = () = > {
  setTimeout(() = > {
    console.log('Task queue function 2')},0)
  for (let i = 0; i < 5000; i++) {
    console.log('For loop of B')}console.log('B Event completed')}let c = () = > {
  setTimeout(() = > {
    console.log('Task queue function 3')},0)
  for (let i = 0; i < 5000; i++) {
    console.log('For loop of C')}console.log('C event completed')
}
a();
b();
c();
// setTimeout will be executed after a, b, and c have all been executed
Copy the code

Macro and micro tasks

Asynchronous tasks are divided into macrotasks and microtasks. Tasks registered by different APIS will enter their corresponding queues one by one, and then wait for the Event Loop to push them into the execution stack for execution.

Macrotask: the code currently executing in the call stack is called a macrotask. (Main code fast, timer, etc.).

Script, setTimeout, setInterval, UI rendering, I/O, postMessage, MessageChannel, setImmediate(Node.js environment)

Microtask: a task that needs to be executed before the next macro task starts after the current macro task (the script as a whole is also a macro task). This can be interpreted as a callback event. (Promise. then, proness.nextTick, etc.).

Promise.then, MutaionObserver, process.nexttick (node.js environment)

Events in macro tasks are placed in the callback queue, which triggers thread maintenance; Microtask events are placed in the microtask queue and maintained by the JS engine thread.

Note: Perform the micro task first, then the macro task.

Summary event loop:

  • All synchronization tasks are executed on the main thread, forming an execution stack.
  • In addition to the main thread, there is a “task queue”. When an asynchronous task is encountered, suspend the task first, and place an event in the “task queue” whenever the asynchronous task has a result.
  • Once all synchronization tasks in the Execution stack are completed, the system reads the Task queue. For those asynchronous tasks, the micro tasks enter the execution stack and start executing, and then the macro tasks enter the execution stack and start executing.

Having looked at the mechanism of the event loop, let’s go back to timers

Timer operation mechanism

The operation mechanism of setTimeout and setInterval is to move the specified code out of the current event cycle and wait until the next event cycle to check whether the specified time is reached. If so, execute the corresponding code; If not, keep waiting.

This means that the callbacks specified by setTimeout and setInterval will not start executing until all synchronization tasks of the current event loop have been completed. Since it is uncertain how long the previous tasks will take to complete, there is no guarantee that the tasks specified by setTimeout and setInterval will be executed at the scheduled time.

setTimeout(someTask, 100);
veryLongTask();
Copy the code

The setTimeout of the above code specifies that a task will run after 100 milliseconds. VeryLongTask (someTask) takes 100 milliseconds to complete, so the task will wait until the veryLongTask is finished.

Let’s do another example of setInterval.

setInterval(function () {
  console.log(2);
}, 1000);

sleep(3000);

function sleep(ms) {
  var start = Date.now();
  while ((Date.now() - start) < ms) {
  }
}
Copy the code

In the code above, setInterval requires a 2 to be printed every 1000 milliseconds. However, if the next sleep statement takes 3000 milliseconds to complete, the setInterval must be delayed until that time. Note that setInterval does not have a cumulative effect, that is, instead of printing three twos at once, it only prints one.

Therefore, in fact, the timer is not accurate, it can be said that the execution time must be greater than the set time.

Interview question Practice

Example 1:

console.log('to 111');
setTimeout(function() {
  console.log('setTimeout111');
});
Promise.resolve().then(function() {
  console.log('promise111');
}).then(function() {
  console.log('promise222');
});
console.log('to 222');
Copy the code

Let’s follow the steps to analyze:

  1. If a synchronization task is encountered, directly print Start 111.
  2. When asynchronous setTimeout is encountered, it is placed in the task queue for execution.
  3. Then that encounters a Promise is placed in the wait queue.
  4. Then that encounters a Promise is placed in the wait queue.
  5. If a synchronization task is encountered, print Start 222.
  6. After synchronous execution, the code in the task queue is returned and executed from top to bottom. Then, the macro task setTimeout and the Promise of the micro task are found. Then, the micro task is executed first and then the macro task is executed.

Therefore, the printing sequence is: Start 111, Start 222, promise111, promise222, and setTimeout111.

Note: For promises, only then() is asynchronous, and new Promise() is synchronous

Example 2:

console.log('to 111');
setTimeout(function () {
  console.log('timeout111');
});
new Promise(resolve= > {
  console.log('promise111');
  resolve();
  setTimeout(() = > console.log('timeout222'));
}).then(function () {
  console.log('promise222')})console.log('to 222');
Copy the code

Analyze:

  1. When you encounter a synchronization code, print “Start 111”.
  2. When a setTimeout asynchrony is encountered, it is placed in a queue and waits for execution.
  3. The Promise function is executed, printing “promise111”.
  4. When setTimeout is encountered, it is asynchronous and put into a queue for execution.
  5. Then that encounters a Promise waits for a successful return, asynchronously, and is queued.
  6. In case of synchronization, print start 222.
  7. When it’s done, it returns and executes the code in the asynchronous queue in order. There is a micro task, then later, so print “promise222” and then perform two macro tasks “timeout111” and “timeout222”.

Therefore, the printing sequence is: Start 111, promise111, Start 222, promise222, timeout111, and timeout222.