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 is used to realize the interactive logic of web pages, involving DOM operation. If multiple threads operate at the same time, they need to do synchronous and mutually exclusive processing. In order to simplify, it is designed as a single thread. 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, requestAnimationFrame, 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 five priorities: Timers, Pending, Poll, Check, and Close.

Explain the five macro tasks:

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

Pending Callback: A Pending Callback for network or I/O exceptions. Some NIux systems wait for an error report, so it must be processed.

Poll Callback: Handles IO data, network connections, and this is what the server handles.

Check Callback: Performs setImmediate callbacks, which typically occur shortly after performing an IO.

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

So the Node.js Event Loop runs like this:

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 loops run a certain number of Timers macro tasks, then all the microtasks, then a certain number of Pending macro tasks, and then all the microtasks. The same goes for the rest of the Poll, Check, and Close macro tasks. (Correction: This was the case before Node 11, but has been changed since Node 11 to perform all microtasks per macro task)

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, a certain number of Timers macro tasks, then all the micro tasks, then a certain number of Pending Callback macro tasks, and then all the micro tasks.

Why is it a certain amount?

Because if there are too many macro tasks in one stage, the next stage cannot be executed all the time, so there is a limit of upper limit, and the remaining Event Loop will continue to be executed.

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 a certain number of Timers, namely setTimeout and setInterval callback. If too many Timers are reserved for next time
  • Microtasks: Perform all nextTick microtasks before performing other normal microtasks
  • Pending phase: Execute a certain number of IO and network exception callbacks, save too many for next execution
  • Microtasks: Perform all nextTick microtasks before performing other normal microtasks
  • Idle/Prepare: A phase for internal use
  • Microtasks: Perform all nextTick microtasks before performing other normal microtasks
  • Poll phase: Perform a certain number of data callbacks to files, connection callbacks to networks, and save too many for next time. If there are no IO callbacks and no callbacks for the Timers and Check phases to handle, the block waits for IO events
  • Microtasks: Perform all nextTick microtasks before performing other normal microtasks
  • Check phase: Perform a certain amount of setImmediate callback, saving too much for another run.
  • Microtasks: Perform all nextTick microtasks before performing other normal microtasks
  • Close phase: A callback that executes a certain number of Close events, leaving too many for next execution.
  • Microtasks: Perform all nextTick microtasks before performing other normal microtasks

It is obviously much more complex than the Event Loop in the browser, but it is understandable after our previous analysis:

Node.js classifies macro tasks as Timers, Pending, Poll, Check and Close from high to low. It also classifies micro tasks, namely nextTick micro tasks and other micro tasks. The execution process is to execute a certain number of macro tasks at the current priority first (the rest are left for the next loop), then execute the process.nexttick microtask, then perform the normal microtask, and then execute a certain number of macro tasks at the next priority. So it goes on and on. There is also a Idle/Prepare phase for the internal logic of Node.js.

Changed the browser Event Loop method of executing one macro task at a time to allow higher priority macro tasks to be executed earlier, but also set a limit so that the next phase is not executed.

In the poll phase, if the poll queue is empty and there are no tasks in the timers and check queues, the blocked queue waits for I/O events instead of idled. This is also designed because the server is primarily dealing with IO, and blocking here allows for early response to IO.

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 was first used to write interactive logic for web pages. In order to avoid the synchronization problem of multiple threads modifying DOM at the same time, it was designed as a single thread. In order to solve the blocking problem of single thread, a layer of scheduling logic, namely Loop Loop and Task queue, was added, and the blocked logic was put into other threads. This supports asynchrony. 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 five macro tasks, namely Timers, Pending, Poll, Check and Close. It also divides two kinds of microtasks, namely process.nextTick microtask and other microtasks.

Node.js Event Loop process is to execute a certain number of macro tasks in the current stage (the remaining tasks are executed in the next cycle), and then execute all micro tasks. There are six stages: Timers, Pending, Idle/Prepare, Poll, Check, and Close. (Correction: This was the case before Node 11, but has been changed since Node 11 to perform all microtasks per macro task)

The Idle/Prepare phase is used internally by Node.js.

In particular, notice the Poll phase. If the Poll queue is empty and timers and Check queues are also empty, the block waits for IO until the timers and Check queues are called back and then the loop continues.

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). Node.js faces more complex environments with higher performance requirements. So the Event Loop design is a little more complicated.