Recently listed some software development basics, plan to understand each point one by one, and write a detailed, illustrated article for each point. This article is about the Event Loop mechanism of JS in browser environment.

Browser thread

We often say that JS is a single-threaded language, but do not forget that the common browser kernel can be multithreaded, multiple threads will continue to communicate, usually there are several threads:

  • GUI rendering process
  • JS engine thread
  • Timer thread
  • Event trigger thread
  • Asynchronous HTTP request threads

Microtask and Macrotask

Miscrotask and Macrotask are rarely mentioned in most articles explaining JS Event Loop, but these two concepts are very important. When I read zone.js Primer, it often mentions these two concepts. At the time, it was also cloudy, which is one of the reasons I wrote this article.

setTimeout(function () {
    console.log('timeout1');
}, 0);

console.log('start');

Promise.resolve().then(function () {
    console.log('promise1');
    Promise.resolve().then(function () {
        console.log('promise2');
    });
    setTimeout(function () {
        Promise.resolve().then(function () {
            console.log('promise3');
        });
        console.log('timeout2')},0);
});

console.log('done');
Copy the code

What does this code output? If you can answer the question quickly, you will probably have learned how to use Event Loop in practice. If you can’t answer the question, you may need to continue reading.

Question: Execute the callback in then() first, or setTimeout() first?

Answer: Do the former first. Because promise.prototype.then () is Microtask and setTimeout() is Macrotask. Why Miscrotask in the first place? Move on

In the JS thread, every call of the program is regarded as a task. All tasks are divided into many types and stored in queues of corresponding types. For easy understanding, I divide these task queues into three types:

  • Micro-task Queue: a callback function that stores microtasks.

  • Macro-task queue: A callback function that stores macroTasks.

  • Other-task queue: This is a queue that I personally abstracted from and does not actually exist, assuming that this queue is used to store all tasks except microTasks and MacroTasks.

The difference between microtasks and Macrotasks is the order in which they are executed. In simple terms, the JS thread processes tasks on the other-Task queue first, then on the micro-task queue, and finally on the Macro-Task queue. As for the specific execution details of JS threads, they will be described in detail later.

The following are common microtasks and MacroTasks:

  • Microtask: Promise. Prototype. Then (), MutationObserver. Prototype. Observe (), etc.

  • Macrotask: setTimeout (), setImmediate (), XMLHttpRequest. The prototype. The onload (), etc.

JS thread Event Loop implementation

As mentioned above, based on my personal understanding, I have drawn a general model diagram of JS Event Loop implementation in the browser environment, with specific meanings as follows:

1 Obtain the task to be executed and go to Step 1.1

1.1 Check whether tasks exist in the other-Task queue. If yes, obtain the earliest task and go to Step 2. Otherwise, go to Step 1.2.

1.2 Check whether there is a task in the Micro-Task queue. If yes, obtain the earliest task and go to Step 2. Otherwise, go to Step 1.3.

1.3 Check whether there are tasks in the Macro-task queue. If so, obtain the earliest task and perform Step 2; otherwise, perform Step 3.

2 Place the task on the Call stack and execute it, and then perform Step 1 (note that all task queues are constantly updated during execution). Because ordinary tasks, microtasks and macroTasks may also exist within the tasks being executed in the Call Stack, the process of executing tasks can be understood as a recursive process. If the recursion is infinite, the tasks to be executed on the Call Stack will accumulate and overflow continuously. This is also the common Maximum Call Stack Size exceeded error).

3 The thread processes other tasks, such as constantly synchronizing the status of the event triggering thread. Once an event is triggered, check whether the event target has a listener task corresponding to the event. If yes, select this task and perform Step 2. It is important to note that the current step is not executed only after step 1.3. The JS thread will certainly synchronize the state of other threads at some point.

Next, if you think about it, you might wonder: How does the JS process update the micro-Task queue and macro-Task Queue?

As I understand it, both micro-Task queues and other-Task queues update “synchronously”, while Macro-Task queues update “asynchronously”. Here’s how macro task Queue updates (using setTimeout as an example) :

  1. The JS thread determines that a MacroTask is a timer and synchronizes the timer to the timer thread.
  2. The timer thread starts the timer received from the JS thread.
  3. At some point (probably during step 1 above) the JS thread will update the Macro-Task queue with some thread such as timer and HTTP request, i.e., if the timing above ends, The JS thread can store the corresponding timer callback to the Macro-Task queue.

How to understand asynchrony in JS

The current common interpretation of asynchrony is probably: make a call, synchronous if you get an immediate result, asynchronous otherwise.

In the JS environment, I personally don’t agree with this interpretation.

First, according to the above explanation, setTimeout(), promise.prototype.then (), HTTP requests, and various browser events are all considered asynchronous. I don’t think so. I don’t think browser events are asynchronous. Here’s why:

// html: <button id="btn">click</button>

// js
var btn = document.getElementById('btn');

setTimeout(function () {
    console.log('timeout')},0);

Promise.resolve().then(function () {
    console.log('promise');
});

btn.addEventListener('click'.function () {
    console.log('click');
});

btn.click();

console.log('done');
Copy the code

If the browser event is asynchronous, no matter what is printed later, the first print must be done, and the actual print result is click done promise timeout.

That is, JS considers browser events not asynchronous.

Thus, my personal interpretation of asynchrony is synchronous invocation if the external conditions required for the invocation are met and immediate results are obtained, otherwise asynchronous invocation.

With this understanding, when we make an HTTP request, assuming that the server returns the request at the speed of light, does the onload method of the XMLHttpRequest object execute immediately? Obviously not, so HTTP requests are asynchronous calls. This is why I did not single out the Event-Task queue when analyzing the task queue in the Event Loop above. Thus, an asynchronous call can be judged as an asynchronous call if it belongs to either a MicroTask or a MacroTask.

digression

Some of you may have noticed the frequent use of “I think” and “I understand” in this article. This is not because I am not confident about myself, but because I want to make a point: When reading other people’s technical articles, be sure to maintain the ability to think independently. Even if the author of the article is a famous leader in the industry, he can’t “believe” for a reason. He must establish a self-explanatory model in his mind for the corresponding technical points. As for the reason why I express this opinion, I find that most articles about JS Event Loop are more or less rough or wrong in the process of looking through a large number of articles. If I only read one of them, I am highly likely to build a wrong Event Loop model. Of course, as FAR as I’m concerned, I might be a little wrong. Anyway, keep thinking for yourself and work with you.

Done. 👊

Refer to the link

  • Philip Roberts: What the heck is the event loop anyway? | JSConf EU 2014
  • Tasks, microtasks, queues and schedules
  • HTLM5 EVENT LOOP DEFINITIONS