Understand Event loops in browsers

What is an Event Loop?

Event Loop is an execution model with different implementations on different platforms. Browsers and NodeJS implement their own Event loops based on different technologies. Note that in the browser, it happens in the render process.

What does the Event Loop do? Why is there such a thing?

We know that JS is single-threaded and executes one task at a time. If the browser is just that, then something like Ajax is synchronous to the browser, blocking other tasks while the request is being made. Of course, this is clearly not the case. While the thread is running, it is obvious that we want the browser to accept and execute new tasks, so the event loop mechanism is introduced. Achieve the effect shown below.

Some concepts in event loops

In the event loop, there are a couple of concepts that we throw out.

Macrotasks: Also called tasks. The callbacks of asynchronous tasks are queued to the Macro task queue, waiting to be called later.

  • setTimeout

  • setInterval

  • SetImmediate (unique) Node

  • RequestAnimationFrame (browser only)

  • I/O

  • UI Rendering (browser only)

    A series of macro tasks constitute a macro task queue, which has the characteristics of queue first in first out.

    Microtasks: microtasks, also called jobs. The callbacks of other asynchronous tasks are sequentially placed in the Micro Task Queue, waiting to be invoked later. These asynchronous tasks include:

  • Process. nextTick (Node only)

  • Promise.then()

  • Object.observe

  • MutationObserver

    A series of microtasks constitute the microtask queue.

Note that the code in the Promise constructor executes synchronously

To understand this process, consider a question: what is the output of the following JS script after it is run?

console.log('script start');

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

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

console.log('script end');
Copy the code

Run this script in the latest version of Chrome and the answer is Script start, script end, promise1, promise2, setTimeout. Next, we will use this code to understand the execution model of the event loop. See this article for an explanation of the original English version

Why is that?

To understand this printing logic, we need to know how event loops handle macro and micro tasks. If this is the first time you’re learning this stuff, it might give you a headache. Take a deep breath… 😅

Each “thread” has its own event loop, so each Web worker thread has its own event loop, so it can execute independently, whereas all Windows under the same origin share an event loop and they can communicate synchronously. The event loop runs continuously, performing all the tasks that are arranged. The event loop has multiple task sources, which have a certain order of execution (like indexedDB defines its own), from which the browser chooses to execute the tasks in each loop. This allows the browser to perform performance-sensitive tasks first, such as user input. Ok ok, follow me to continue 🤓. (This means that both macro and microtask queues execute tasks from the source, and the browser selects tasks from both sources and executes them on the call stack when appropriate.)

Macro tasks are planned so that the browser can access JavaScript/DOM internally and ensure that these operations occur sequentially. Between macro tasks, the browser can render updates. Entering the event callback from the mouse click requires scheduling tasks, as does parsing HTML, again with setTimeout above. All three are macro tasks.

In the example above, setTimeout is delayed and scheduled to perform the callback in the next macro task. This is why setTimeout is printed after Script end, because Script end is executed in the first macro task, and setTimeout is executed in another macro task. That’s good to see, come on, keep going… 😋

Typically, microtasks are scheduled for things that happen immediately after the script is currently executed, such as reacting to a batch of actions, or doing something asynchronous that doesn’t take up the entire macro task. In an event loop, if there are no other JS scripts in the call stack that need to be executed, the tasks in the microtask queue will be executed in the call stack. New microtasks are added to the end of the microtask queue. Those added first are executed first, and those added later are executed later. Microtasks include the Mutation Observer callback, as well as the promise callback above. (I can’t bear it, add my own understanding, you can see the original 😇)

Once a promise is created, it is placed at the end of the microtask queue, waiting for the callback to be executed. This ensures that the promise is invoked asynchronously, even though the promise has already been established. The **. Then of the microtask is completed before the next microtask, meaning that the microtasks are executed in sequence. The reason why promise1 and promise2 print after script end is because microtasks wait until the currently running script finishes executing. Promise1 and promise2 print before setTimeout** because the microtask takes place before the next macro task. The execution of a microtask is between two macro tasks, and the tasks in the microtask team are executed sequentially. To help you understand, be sure to follow this page step by step. Animation diagram of execution. Jake, by the way, is an interesting guy.

To summarize, when the browser executes a piece of code, it starts a macro task, and it puts normal code on the call stack, macro task on the macro task queue, and micro task on the micro task queue. After the current macro task code runs, that is, the call stack is empty, the micro tasks in the micro task queue will be taken out and put into the call stack for execution. When all the micro tasks in the micro task queue are executed, the next macro task will be taken out from the macro task queue and put into the call stack for execution. One thing to note here is that if a microtask keeps being added while it is running, the microtask will keep running until all the microtasks have been run before the macro task will be run.

Some browsers print in a different order?

Some browsers print script start, script end, setTimeout, promise1, promise2, and they run the promise callback after setTimeout. These browsers invoke promises as a macro task rather than a microtask.

Using promises as macro tasks can cause performance issues and may cause unnecessary delays, such as rendering being delayed. It also causes uncertainty due to interactions with other task sources and can break interactions with other apis. Safair and Firefox fixed the issue in later versions because of this effect. (Meaning, later browsers came to treat promises as microtasks.)

Please point out any misunderstandings. If this article doesn’t help you, check out the following two JSconf videos.

  • The event loop system in JavaScript
  • IN THE LOOP

You don’t need to remember, but you should be clear (Zeng Guofan)