Event Loop is a basic concept of JavaScript. It is a must in an interview and often mentioned in daily life. But have you ever wondered why there is an Event Loop and why it is designed like this?

Today we’re going to explore why.

Event Loop for the browser

JavaScript was originally designed to do form verification in the browser. In order to simplify the language design, we do not want to introduce multithreaded API to increase the complexity, so only a single JS thread is supported. However, if a single thread is used, it will be blocked by timing logic and network requests. What to do?

You can add a layer of scheduling logic. The JS code is encapsulated into a task, put in a task queue, the main thread will continue to take the task execution.

Each time a fetch task executes, a new call stack is created.

The timer and the network request are actually executed by another thread. After the execution, a task is put in the task queue to tell the main thread to continue to execute.

This Loop is called an Event Loop because the asynchronous task is executed by another thread, and then the main thread is notified by the task queue.

These asynchronous tasks performed on other threads include timers (setTimeout, setInterval), UI rendering, and network requests (XHR or FETCH).

However, there is a serious problem with the current Event Loop. There is no concept of priority and the Event Loop is only executed in sequence. Therefore, tasks with high priority cannot be executed in time. So, you have to design a queue-jumping mechanism.

Instead, create a high-priority task queue. For every normal task, complete all high-priority tasks before performing normal tasks.

With queue-jumping mechanism, high quality tasks can be carried out in time.

This is the Event Loop of the current browser.

Common tasks are called macrotasks and advanced tasks are called microtasks.

Macro tasks include: setTimeout, setInterval, Ajax, FETCH, script tag code.

Microtasks include: Promise.then, MutationObserver, object.observe.

How to understand the division of macro and micro tasks?

Timers, network requests, and the like are common asynchronous logic that notifies the main thread after another thread has finished running, so they’re all macro tasks.

MutationObserver and Object.observe monitor the change of an Object. The change is instantaneous, so you must respond immediately, or it may change again. Terminating the call then asynchronously is also preferable.

This is the design of the Event Loop in the browser: The Loop mechanism and the Task queue are designed to support asynchronism and solve the problem of logical execution blocking the main thread, and the queue jumping mechanism of the MicroTask queue is designed to solve the problem of high quality tasks executing early.

But later, JS execution environment is not only a browser, but also Node.js, which also has to solve these problems, but it designs the Event Loop more detailed.

Node. Js Event loop

Node.js is a new JS runtime environment, which also supports asynchronous logic, including timer, IO, network request, and obviously can also use the Event Loop.

However, the browser Event Loop was designed for browsers, which is a bit crude for high-performance servers.

Where is it rough?

The browser Event Loop has only two priority levels, one for macro tasks and one for micro tasks. But there is no re-prioritization of macro tasks, and there is no re-prioritization of micro tasks.

For example, the Timer logic has a higher priority than the IO logic, because it involves time, the earlier the more accurate; However, the processing logic of close resources is of very low priority, because not close will occupy a lot of resources such as memory, which has little impact.

The macro task queue is split into four priorities: Timers, IO Callback, Check, and Close Callback.

Explain the four macro tasks:

Timers: When it comes to time, the earlier the execution is definitely more accurate, so it is easy to understand that this is the highest priority.

IO Callback: In addition to Timers, IO has the highest priority.

Close Callback: Closes the resource Callback, and has the lowest priority

Check: Performs some custom logic, but has a higher priority than Close, which is the least important, so comes before it.

So the Node.js Event Loop runs like this:

Node.js has a thread that runs asynchronous logic, just as asynchronous tasks such as run timers and network requests are handled by other threads in browsers, but the asynchronous logic of IO is managed by a thread pool called Libuv.

One other difference to note:

Instead of executing one macro task at a time and then all the microtasks in the browser, node.js Event Loop executes all Timers macro tasks and then all the microtasks, then all IO Callback macro tasks and then all the microtasks. The rest of the Check and Close Callback macro tasks do the same.

Why is that?

It’s easy to understand in terms of priorities:

Let’s say the macro tasks in the browser have priority 1, so they are executed in order of precedence, so one macro task, then all the microtasks, then one macro task, then all the microtasks.

Node.js macro tasks also have priority, so node.js Event Loop will run all the macro tasks of the current priority before running the micro task, and then run the macro task of the next priority.

That is, all Timers macro tasks, then all microtasks, then all IO Callback macro tasks, then all microtasks and so on.

In addition to macro tasks having priority, microtasks are also prioritized, with a new high priority microtask called Process. nextTick, which runs ahead of all normal microtasks.

So, the complete flow of node.js Event Loop looks like this:

  • Timers stage: Executes all Timers, namely setTimeout and setInterval callback
  • Microtasks: Perform all nextTick microtasks before performing other normal microtasks
  • IO Callback stage: Performs all Callback except Timers and Close
  • Microtasks: Perform all nextTick microtasks before performing other normal microtasks
  • Idle/Prepare: an internal phase, not a macro task
  • Check phase: Perform all setImmediate callbacks
  • Microtasks: Perform all nextTick microtasks before performing other normal microtasks
  • Close Callback stage: Callback to execute all Close events
  • Microtasks: Perform all nextTick microtasks before performing other normal microtasks

This is obviously much more complex than the Event Loop in the browser, but it’s easy to understand after our previous analysis:

Node.js classifies macro tasks as Timers, IO Callback, Check and Close Callback from high to low. It also classifies micro tasks, namely nextTick micro tasks and other micro tasks. The execution process is to execute all macro tasks of the current priority, then process.nexttick microtask, then normal microtask, and then execute all macro tasks of the next priority. So it goes on and on. The Idle/Prepare phase is used for the internal logic of Node.js. It is not a macro task.

The complete Node.js Event Loop looks like this:

Compare the browser Event Loop:

The overall design idea of Event Loop in the two JS running environments is similar, but node.js Event Loop makes finer granularity division of macro tasks and micro tasks, which is easy to understand. After all, Node.js is oriented to different environments and browsers. More importantly, the performance requirements on the server side will be higher.

conclusion

JavaScript is designed to be simple and supports only a single JS thread, but in order to solve the blocking problem, a layer of scheduling logic, namely Loop loops and Task queues, is added to allow the blocking logic to run on other threads, thus supporting asyncrony. Then, to support high-priority task scheduling, microtask queues were introduced, which is the browser’s Event Loop mechanism: execute macro tasks one at a time, and then execute all microtasks.

Node.js is also a JS runtime environment, and if you want to support async, you also need to use Event Loop. However, the server environment is more complex and requires higher performance, so Node.js makes finer granularity priority division for both macro and micro tasks:

Node.js is divided into 4 macro tasks, namely Timers, IO Callback, Check and Close Callback. It also divides two kinds of microtasks, namely process.nextTick microtask and other microtasks.

The Node.js Event Loop process executes all macro tasks of the current stage, followed by all micro tasks. The four stages (Timers, IO Callback, Check, and Close Callback) are executed in this order. Specifically, all macro tasks for TImers, then all nextTick microtasks, then all normal microtasks, then all macro tasks for the IO Callback phase…

Event Loop is a set of scheduling logic designed by JS to support asynchrony and task priority. It has different designs for different environments such as browser and Node.js (mainly the granularity of task priority division is different), but the overall idea is similar.