Know in advance

Single thread

It is well known that the JavaScript language is single-threaded, meaning that only one thing is being done at a time. Why? As a browser scripting language, JavaScript can cause complex synchronization problems when manipulating the DOM if it has multiple threads at the same time. For example, two threads of JavaScript operate on the same DOM node, one changing its background color to blue and the other to black. So, what should the browser do first? So, to avoid this, it has to be single-threaded.

Synchronous and Asynchronous Tasks

Because it is single-threaded, all JavaScript tasks are queued. The second task is executed only after the first task is completed. However, there is a problem: when the first task takes a long time, the second task has to wait forever. To solve this problem, the designers of the JavaScript language split all tasks into two categories:

  • Synchronization task

Tasks queued for execution on the main thread. The next task can be executed only after the previous task is executed.

  • Asynchronous tasks

Tasks that do not feed into the main thread but into the task queue. An asynchronous task is executed on the main thread only when the task queue notifies the main thread that it is ready to execute.

Task Queue

A task queue is a queue of events (also known as a message queue). It is a first-in, first-out data structure, and the first events are read first by the main thread.

In an event loop, there is at least one task queue, and a task queue is a collection of ordered tasks. Each task has a task source, and tasks from the same task source must be placed in the same task queue. In other words, tasks are added to their respective queues according to the task source. The setTimeout and Promise apis, for example, can be thought of as two different task sources, and their registered tasks will be placed in their respective queues.

Event Loop

To the point


figure 1.1 : J S Operating mechanism diagram Figure 1.1:JS operation mechanism diagram

Note: The rotation icon in the figure can be understood as the main thread loop constantly reading the task queue.

In the figure above, when the main thread is running, the heap and stack are generated. The code in the stack calls various external apis, which add various events (click, load, done, etc.) to the task queue. As soon as the stack completes, the main thread reads the task queue and executes the corresponding callback function for those events. This process repeats itself (unless you close the page), which is why it’s called an Event Loop. In general, it can be summarized as the following steps:

  1. All synchronization tasks are executed on the main thread, forming an execution stack.

  2. Outside of the main thread, there is a task queue. Once the asynchronous task has a run result, an event is placed in the task queue.

  3. When all synchronization tasks in the stack are completed, the system reads the task queue to check the events. Those corresponding asynchronous tasks end the wait state, enter the execution stack and begin execution.

  4. The main thread repeats step 3 above.

For example

For the sake of understanding, let’s assume the following case:

While the page is loading, the user moves quickly and clicks the mouse, triggering two events: a mouse move and a mouse click.

The following is a simple flowchart of the main thread and task queue based on the above example.


figure 1.2 : Schematic diagram of main thread and task queue Figure 1.2: Schematic diagram of main thread and task queue

In the event processing phase, when the event loop checks the queue, it finds a mouse movement event at the head of the queue and executes its corresponding event handler (event callback). When the handler has finished executing (that is, after the last line of code has been executed), the JS engine exits the current event handler function and the event loop checks the queue again. At this point, at the front of the queue, it finds the mouse-click event and processes it. After the clicked callback completes, the event loop continues to loop, waiting to process incoming new events. This cycle continues until the user closes the Web application.

Macrotasks and Microtasks

Tip: In ECMAScript, macro tasks and micro tasks are called: Task and Jobs, respectively.

Macro task

Code that is understood to execute on each execution stack is a macro task (including fetching event callbacks from the event queue and placing them on the execution stack at a time).

Mainly include:

  1. The main block of code
  2. setTimeout
  3. setInterval
  4. RequestAnimationFrame (Animation, detail)
  5. SetImmediate (API in Node)

Micro tasks

Microtasks are smaller tasks. Microtasks update the state of the application, but must be performed before the browser task (for example, rerendering the UI of a page) continues to perform other tasks. We can think of a microtask as a task that executes immediately after the execution of the current macro task, always following the macro task.

Mainly include:

  1. Promise.then()
  2. catch
  3. finally
  4. Object.observe
  5. MutationObserver (provides the ability to monitor changes made to the DOM tree, detailed)
  6. Process.nexttick (API in Node)

We already know that tasks from the same task source (for example, setTimeout/Promise apis) are put into the same task queue. So, in an event loop, event callbacks for macro tasks and microtasks are placed in the corresponding macro task queue and microtask queue. This tells us something: event loops should usually contain at least one macro task queue and one microtask queue. A picture is worth a thousand words. Having said that, let’s go straight to the picture.

Tip: there are so many browsers and JavaScript execution environments these days that you might encounter an event loop where all the tasks are in a queue, so don’t be surprised.


figure 1.3 : Event loops that contain macro tasks and microtask queues Figure 1.3: Event loop with macro and micro task queues

Single loop iteration steps:

  1. The event loop first checks the macro task queue and starts execution immediately if there is a macro task waiting. Until the task completes (or the queue is empty), the event loop moves to process the microtask queue.

  2. If there are tasks waiting in the microtask queue, the event cycle will start to execute successively, and the next microtask will be executed only after the current one is completed, until all the microtasks in the microtask queue are completed.

  3. When all the microtasks in the microtask queue are completed and emptied, the event loop will check whether the UI rendering needs to be updated. If not, the current event loop will end and a new event loop will start. If so, the UI view is re-rendered and then a new event loop is started.

Note:

  • Event loops handle the difference between macro tasks and microtask queues: in a single iteration of the loop, at most one macro task is processed (the rest wait in the queue), while all microtasks in the queue are processed.

  • Both types of task queues are independent of the event loop. That is, adding to the task queue takes place outside of the event loop. This is designed to prevent any events that occur from being ignored while executing the JavaScript code. JavaScript is single-threaded, so both macro and micro tasks are executed one by one. When a task is started and not completed, it is not interrupted by any other task. If it is not independent of the event loop, the task cannot be detected.

  • All microtasks are executed after the macro task and before the next render because the microtask updates the application’s state before rendering.

Macro and micro task cases

As shown in Figure 1.2, the case mentioned above: while loading the page is not complete (js main thread is not completed), the user moves quickly and clicks the mouse, triggering two events: one mouse move and one mouse click.

To deepen our understanding of macro and micro tasks, here I divide this example into two different cases:

  1. Only macro tasks.
  2. Both macro and micro tasks exist.

About the code part, not very rigorous, just for reference

Only macro tasks

The sample code

document.addEventListener("mousemove".function firstHandler() {
    for (var i = 0; i < 4; i++) {
        console.log('Mousemove -- Start execution:' + i);
    }
    console.log('Mousemove -- Execution finished');
});
document.addEventListener("click".function secondHandler() {
    for (var i = 0; i < 2; i++) {
        console.log('click -- start execution: ' + i);
    }
    console.log('click -- end of execution ');
});
const num = 40000;
for (var i = 0; i < num; i++) {
    console.log('js main thread -- start execution: ' + i);
}
console.log('JS main thread -- end of execution');
Copy the code

Running result diagram

Const num = 40000; If the value is too small, the reaction speed of the trigger event is too late. If the value is too large, it is easy to get stuck. 40000 is not ideal. It took several attempts to record the trigger process in good condition (too much trouble, I did not adjust a proper value).

The event monitoring and add tasks are independent of the event loop, and we can still add tasks to the queue even if the main thread is not finished executing.

As we can see from the figure above, since JS is single-threaded, once a task is executed, it cannot be interrupted by another task. When we move and click the mouse while the main thread JS code is still executing (both events, which are not fired at the same time with a little time interval), they do not immediately execute the corresponding handler (callback function). Instead, the tasks are queued one by one (first-in, first-out) and executed one by one after the main thread is finished.

For ease of understanding, let’s assume:

  • The main thread JS code should run for 10ms.
  • The mouse movement event handler needs to run 8ms.
  • The mouse click event handler takes 5ms to run.
  • Re-render the page for 0ms.

Based on this premise, draw a left-to-right timeline (milliseconds) containing only macro tasks and part of the JS code executed in the corresponding time period. Because there are no microtasks, the microtask queue is empty.


figure 1.4 : Macro task diagram Figure 1.4: Macro task diagram

Both macro and micro tasks exist

The sample code

document.addEventListener("mousemove".function firstHandler() {
    for (var i = 0; i < 4; i++) {
        console.log('Mousemove -- Start execution:' + i);
    }
    Promise.resolve().then(() = > {
         for (var i = 0; i < 2; i++) {
            console.log('Promise microtask -- Start: ' + i);
         }
         console.log('Promise Microtask - End ');
    }); 
    console.log('Mousemove -- Execution finished');
});
document.addEventListener("click".function secondHandler() {
    for (var i = 0; i < 2; i++) {
        console.log('click -- start execution: ' + i);
    }
    console.log('click -- end of execution ');
});
const num = 40000;
for (var i = 0; i < num; i++) {
    console.log('js main thread -- start execution: ' + i);
}
console.log('JS main thread -- end of execution');
Copy the code

The only difference between this code and the code for the macro task above is the inclusion of the promise in the mouse movement handler (event callback) and the processing of the promise when it is fulfilled.

Running result diagram

Combining the code with the results shown above, we can see that the microtasks created during the mouse movement are put into the microtask queue. After the mouse movement completes, the event loop detects that there are microtasks in the microtask queue, so it takes precedence over the macro task waiting in the queue: mouse click. It will be easier to understand when combined with Figure 1.3.

For ease of understanding, suppose again:

  • The main thread JS code should run for 10ms.
  • The mouse movement event handler needs to run 8ms.
  • The promise is created and fulfilled, and runs 2ms.
  • The mouse click event handler takes 5ms to run.
  • Re-render the page for 0ms.

On this premise, draw a left-to-right timeline (millisecond) containing both macro and micro tasks and some JS code executed in the corresponding time period.


figure 1.5 : Illustration of macro and micro tasks Figure 1.5: Illustration of macro and micro tasks

Timer in an event loop

SetTimeout () and setInterval() are two timers that operate exactly the same way. The difference is that the code specified by setTimeout() is executed only once, while the code specified by setInterval() is executed repeatedly. The detailed information about them will not be introduced too much here, but if you are interested, you can check it out by yourself. Now, let’s go straight to the example.

The sample code

setTimeout(function timeoutHandler() {
    console.log('Register: setTimeout');
}, 500);
setInterval(function intervalHandler() {
    console.log('Register: setInterval');
}, 500);
// Register event handlers for button click events
const btn = document.getElementById("myBtn");
btn.addEventListener("click".function clickHandler() {
    console.log('Click event: click');
});

const num = 200;
for (var i = 0; i < num; i++) {
    console.log('js main thread -- start execution: ' + i);
}
console.log('JS main thread -- end of execution');
Copy the code

When the main thread code is executed, three events occur: mouse click event (click while the main thread is not finished), setTimeout expiration event, and setInterval trigger event. Take a look at the diagram below and observe the order in which they are executed.

Running result diagram

The delay time parameters of setTimeout and setInterval specify only the time when the timer is added to the queue, not the exact execution time.

Both timers are called when the main thread is running, but each has its own delay time, and only when their respective delay time expires will the corresponding task be added to the queue. So after the main thread is finished, why is the mouse click performed first? As we mentioned earlier, both timers are macro tasks. Mouse clicks are macro tasks that are triggered while the main thread is still executing and before their latency expires, so they are added to the top of the macro task queue.

However, as they are added to the queue, setInterval is continuously added to the queue (unless cleared) because it is executed repeatedly, and not every time the delay expires. The reason for this is that tasks take a certain amount of time to execute, and browsers do not create two identical interval timers at the same time. What does that mean? That is, if there is already an instance of setInterval waiting in the queue, and setInterval is ready to trigger, the triggering will be aborted. Now you can see why setInterval delays are sometimes inaccurate. If you don’t understand, it doesn’t matter. Look at the picture.

Take the above case as an assumption:

  • The main thread executes 520ms.
  • At 0ms, setTimeout and setInterval delay execution by 500ms. And both run 500ms
  • At 6ms, click to run 500ms.
  • At 500ms, the setTimeout timer expires and the first interval of the setInterval timer is triggered.
  • Re-render the page for 0ms.

If this graph is any guide, the second setInterval will fire after 2000ms. Not every delay is inaccurate, of course, and over subsequent intervals it stabilizes at about once every 500ms (depending on whether the code execution is interrupted or not). However, the setTimeout expiration event that was supposed to be executed after 500ms can only be executed after 1020ms due to the main thread execution and mouse click event.

Both of them indicate that we can’t be sure that the timer handler will execute at exactly the time we expect. At the same time, there are differences between the two timers:

  • Depending on the state of the event queue, the actual wait time will only be longer than the set delay time.
  • SetInterval will attempt to execute the callback every N milliseconds (the set delay time), regardless of whether the previous callback executes.

conclusion

This article mainly describes the principle of the event loop, and does not extend too much to explain in detail. If you say something wrong, please kindly advise. Those who want to learn more about the mechanics are advised to read (and are the main reference articles and books in this article) :

  1. More on Event Loop

  2. Javascript Ninja Secrets, 2nd edition, Chapter 13 — Deeper into the event loop