JavaScript is a single-threaded language, which means it can only perform one task at a time. If there are multiple tasks, they must be queued up, waiting for the completion of the preceding task before executing the next one. The advantage of this mode is that it is relatively simple to implement and the execution environment is relatively straightforward. However, the downside is that if one task takes a long time, the subsequent tasks have to wait in line, delaying the execution of the entire program. The common unresponsiveness of the browser, also known as a deadlock state, is often caused by a piece of JavaScript code running for a long time, such as an infinite loop, which hinders the execution of other tasks.
To address the aforementioned issue, JavaScript divides the execution mode into two types: synchronous and asynchronous. The terms synchronous and asynchronous refer to whether the entire process needs to be completed in order and whether the functions you call will immediately return the result, respectively.
Synchronous mode operates as synchronous blocking, where the subsequent task waits for the preceding task to finish before being executed. The execution sequence of the program is consistent with the order of the tasks, being synchronous.
Asynchronous execution operates in a non-blocking mode, where each task has one or more callback functions. Instead of executing the next task after the completion of the preceding one, it executes the callback function, and the subsequent task does not wait for the preceding task to complete before execution. Therefore, the execution sequence of the program is inconsistent with the order of the tasks, being asynchronous. Each browser allocates only one JavaScript thread per tab, with the main tasks being user interaction and DOM manipulation, which is why it must be single-threaded to avoid complex synchronization issues. For example, if JavaScript had two threads at the same time, one thread adding content to a DOM node while the other thread deleting that node, the browser would be unable to determine which thread's operation to follow.
Let's first look at an example, to test an asynchronous operation as mentioned in the previous content.
In local testing, the setTimeout
callback function executes approximately 30s later, far exceeding 4ms. I intentionally set a very large loop in the main thread to block the JavaScript main thread. Please note that I did not set an infinite loop here. If I were to set an infinite loop here to block the main thread, the setTimeout
callback function would never execute. Furthermore, since the rendering thread and the JS engine thread are mutually exclusive, when the JS thread is processing a task, the rendering thread is suspended, causing the entire page to be blocked and unable to refresh or even close. The only way to close the page is by using the task manager to end the Tab process.
JavaScript implements asynchronous behavior through an execution stack and a task queue. All synchronous tasks are executed on the main thread, forming an execution stack, and various event callbacks (also known as messages) are stored in the task queue. After the tasks in the execution stack are completed, the main thread starts to read and execute the tasks in the task queue, repeating this process endlessly.
The main thread continuously reads events from the task queue, hence this mechanism is referred to as the "Event Loop". The Event Loop is an execution model with different implementations in different environments. Browsers and NodeJS have implemented their respective Event Loop based on different technologies. The Event Loop in the browser is explicitly defined in the HTML5 specification, while the Event Loop in NodeJS is based on the implementation of libuv.
In the browser, the Event Loop consists of the execution stack, background threads, macrotask queue, and microtask queue.
When JS is executed, it follows the steps as below:
// Execution stack: setTimeout // Microtask queue: [] // Macrotask queue: [setTimeout1]
// Execution stack: Promise // Microtask queue: [then1] // Macrotask queue: [setTimeout1]
// Execution stack: setTimeout // Microtask queue: [then1] // Macrotask queue: [setTimeout1 setTimeout2]
// Execution stack: console // Microtask queue: [then1] // Macrotask queue: [setTimeout1 setTimeout2]
// Execution stack: then1 // Microtask queue: [] // Macrotask queue: [setTimeout1 setTimeout2]
// Execution stack: setTimeout1 // Microtask queue: [then2] // Macrotask queue: [setTimeout2]
// Execution stack: then2 // Microtask queue: [] // Macrotask queue: [setTimeout2]
// Execution stack: setTimeout2 // Microtask queue: [] // Macrotask queue: []