When I learned about event loops, I tried to find some specifications to learn about, but when I looked through EcmaScript or V8 they didn’t have a definition for this thing. For example, in V8 there was execution stack, heap, etc. Indeed, the event loop is not here.

Later, I gradually learned that in the browser environment, the definition of event loops is in the HTML standard. Previously, the HTML specification was developed by THE WHATWG and the W3C, both of which have their own differences. In 2019, the two organizations signed an agreement to collaborate on a single version of HTML and DOM, and the HTML and DOM standards were eventually maintained by the WHATWG.

This article focuses on the WHATWG Standard for HTML Living Standard Event Loops, which defines how the browser kernel should implement it.

Event loops in the browser specification

Event loop definition

To coordinate events, user interactions, scripting, rendering, networking, and so on, the user agent must use the event loop described in this section. Each agent has an associated event loop that is unique to each agent.

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each agent has an associated event loop, which is unique to that agent.

It can also be seen from this definition that the event loop is mainly used to coordinate the operation mechanism between events, the network, JavaScript, etc. Let’s take JavaScript as the starting point to see how they interact.

An important concept in the event loop is the task queue, which determines the order in which tasks are executed.

The processing mode of the event loop

The specification 8.1.6.3 processing model defines the event loop processing mode. When an event loop exists, it continuously performs the following steps:

These concepts are hard to understand, but to summarize:

  • To perform a Task: taskQueue there are multiple task source queues (DOM, UI, network, etc.) from which at least one runnable task is selected and placed in the taskQueue.
    • If you do not jump directly to the microtask queue, you will not go through 2 to 5.
    • Otherwise, the first executable task from the taskQueue is executed as an oldestTask, corresponding to 2 to 5.
    • Note that microtasks are not selected here, but when a task queue contains a microtask, it is added to the microtask queue.
  • Execute Microtask: Execute the Microtask queue until the Microtask queue is empty, where scheduling too many microtasks can also cause blocking.
  • Update render.

Task, Microtask and Render are the three stages of an event cycle, which will be discussed later.

Photo source:Pic2.zhimg.com/80/v2-38e53…

Task (Macrotask)

I’ve read a lot of articles about event loops, ** most of them refer to “Task” as “Marcotask”, which is a macro Task, but there is no “Marcotask” in the specification. ** Because there is no such term in the specification, I put a parenthesis in the title, there are many names. There’s also something called an external queue, which actually means the same thing, and if you’re new to learning about event loops you might wonder why I can’t find an explanation for this.

I will continue to use the term “task queue” from the specification.

A task queue is a collection of tasks. An event loop has one or more task queues, and the first step the event loop does is to fetch the first runnable task from the selected queue rather than to list the first task.

The traditional Queue is a first-in, first-out data structure, and the first one is always executed first. However, the Queue here contains some delayed tasks such as setTimeout, so there is a sentence in the specification: “Task queues are sets, not queues”.

The task sources of the task queue mainly include the following:

  • DOM manipulation: Tasks that result from DOM operations, such as inserting elements into a document in a non-blocking mannerdocument.body = aNewBodyElement;.
  • User interaction: Tasks generated by user interaction, such as Callback tasks generated by mouse click and movement.
  • Network: Tasks generated by network requests, such as fetch().
  • History traversal: This task source is used to queue calls to history.back() and similar apis.
  • **setTimeout and setInterval: tasks related to timers.

For example, when the User Agent has a task queue that manages mouse and keyboard events and another task queue associated with other task sources, it will spend three quarters more time in the event loop preferentially executing the task queue for mouse and keyboard events than any other task. In this way, the tasks related to user interaction can be processed with higher priority while the task queues of other task sources can be processed, which also improves user experience.

Microtask

Each event loop has a microtask queue, which is not a task queue, but a separate queue.

What are microtasks?

A microtask is a short function that fires when the function that created it executes and the JavaScript execution context stack is empty before control is returned to the event loop.

As we continue to create more tasks to the microtask queue via queueMicrotask(callback) within a microtask, for the event loop, it will continue to call the microtask until the queue is empty.

const log = console.log;
let i = 0;
log('sync run start');
runMicrotask();
log('sync run end');

function runMicrotask() {
  queueMicrotask(() = > {
    log("microtask run, i = ", i++);
    if (i > 10) return; 
    runMicrotask();
  });
}
Copy the code

The runMicrotask() function is called on the main thread, which internally uses queueMicrotask() to create a microtask and recursively calls it. The microtask is triggered when the stack is empty because the recursive call generates a new microtask each time. The event loop also executes the setTimeout callback in the Task Queue after the microtask completes.

sync run start
sync run end
microtask run, i = 0
microtask run, i = 1
microtask run, i = 2
microtask run, i = 3
microtask run, i = 4
microtask run, i = 5
microtask run, i = 6
microtask run, i = 7
microtask run, i = 8
microtask run, i = 9
microtask run, i = 10
Copy the code

From this example, you can also see that scheduling a large number of microtasks can cause the same performance defects as synchronous tasks, with subsequent tasks not being executed and browser rendering prevented. The queue here is the real queue.

Create a Microtask (Promise VS queueMicrotask)

In the past, it was very easy to create a microtask. You could create a Promise that immediately resolves. Each time you create a Promise instance, you create an extra memory overhead, and the Promise throws a nonstandard Error. If it is not normal to capture usually get such a mistake UnhandledPromiseRejectionWarning:.

Use Promise to create a microtask.

const p = new Promise((resolve, reject) = > {
  // reject('err')
  resolve(1);
});
p.then(() = > {
  log('Promise microtask.')});Copy the code

The queueMicrotask() method is now provided on the Window object in a standard way to safely introduce microtasks without additional tricks, providing a standard exception.

Create a microtask using queueMicrotask().

queueMicrotask(() = > {
  log('queueMicrotask.');
});
Copy the code

When we write business functions, it is also common to have multiple asynchronously scheduled tasks within a function or method, which we are familiar with based on promises, and to use Async/Await to write code in a synchronous linear manner. QueueMicrotask, on the other hand, needs to pass a callback function, which is easy to nest when there are too many levels.

The point is that most of the time we don’t need to create microtasks, too much abuse can cause performance problems, and you may need to use microtasks to achieve certain functions when doing things like creating frameworks or libraries. Here comes the oft-asked interview question “Make a Promise”, which might be implemented using queueMicrotask(), as you’ll see again in the source code series for Asynchronous JavaScript Programming.

Microtask summary

Microtask is summed up in one sentence: “It is executed before the next event loop at the end of the current execution stack.” Note that if the Microtask queue is not empty, it will continue to execute the Microtask. For example, using recursion to continuously add new microtasks, this is bad.

The task sources contained in microtasks are not clearly defined and usually include promise.then (), Object.observe (deprecated), MutaionObserver, and queueMicrotask.

Update the rendering

Rendering is another important stage in the event cycle. Here is a good explanation of how the browser works. The entire rendering process is mainly described in the following steps.

  • Parsing an HTML document into a DOM Tree also parses an external CSS file and an embedded CSS styled CSSOM Tree.
  • The combination of DOM Tree and CSSOM Tree creates another Render Tree structure.
  • After the Render Tree is complete, enter the Layout stage, assigning each node a coordinate position on the screen.
  • Next, the entire page is painted based on node coordinate positions.
  • Layout and Repaint are also triggered when we make changes to DOM elements, such as changing the color of the element or adding DOM nodes.

Photo source:www.html5rocks.com/zh/tutorial…

Look at the rendering process with Task and Microtask

Using queueMicrotask to create a microtask, I recursively called the custom runMicrotask() function 10 times. Each time I wanted to change the background color of the container div back and forth, I’ve also put a setTimeout that belongs to the Task Queue just to give you a sense of the order in which the Task Queue is executed in the event loop.

<div id="container" style="width: 200px; height: 200px; background-color: red; font-size: 100px; color: #fff;">
  0
</div>
<script>
  let i = 0;
  const container = document.getElementById('container');
  setTimeout(() = > {});
  runMicrotask();
  function runMicrotask() {
    queueMicrotask(() = > {
      if (i > 10) return; 
      container.innerText = i;
      container.style.backgroundColor = i % 2= = =0 ? 'blue' : 'red';
      runMicrotask();
    });
  }
</script>
Copy the code

Render the final result. If you follow the example above, we might think that there should be one render for each microtask.

The runMicrotask() function is displayed in purple. The Layout function is displayed in purple. The Paint function is displayed after all the microtasks have been executed. After that, the next event loop finally executes the Task Queue Timer.

As described in the event loop processing pattern specification, rendering is run after the completion of an event loop microtask. The above example almost validates this result, but there is a question: Why not execute it after every microtask, when you replace queueMicrotask with setTimeout, not on every event?

When is Render executed in the event loop?

There is also a description in the specification that gets the message that rendering may not be performed at the end of each event loop.

This time is fast if there is no blocking operation for each round of the event loop. Considering hardware refresh frequency limits and user Agent throttling for performance reasons, the browser’s update rendering will not be triggered in each event loop. If the browser is trying to achieve a refresh rate of 60Hz per second, also known as 60 frames per second, the interval between drawing a frame is 16.67ms (1000/60). If you have multiple DOM operations in 16ms, you won’t render multiple times.

If the browser can’t maintain 60fps it will drop to 30fps, 4fps or even lower.

If you want to do a draw within each event loop or after a microtask, you can re-render using requestAnimationFrame.

Look at the rendering process with requestAnimationFrame

RequestAnimationFrame is an API provided under the browser window object that tells the browser I need to run an animation. This method requires the browser to call the specified callback function to update the animation before the next redraw.

Modify the above example to add the requestAnimationFrame() method.

function runMicrotask() {
    queueMicrotask(() = > {
      requestAnimationFrame(() = > {
        if (i > 10) return; 
        container.innerText = i;
        container.style.backgroundColor = i % 2= = =0 ? 'blue' : 'red';
        i++;
        runMicrotask();
      });
    });
  }
Copy the code

After the run, each element change is redrawn as shown below.

Zoom in on one of these to see how the task is performing. RequestAnimationFrame can also be viewed as a task and you can see it performing a microtask after it runs.

Render summary

The Render phase of an event loop may run within a single event loop or after multiple event loops. It can be affected by the browser’s refresh rate, which is 16.67ms per 60fps, or by the browser’s perception that an updated render is not necessary if it doesn’t affect the user.

In general, the mechanism is browser-specific, so you don’t have to worry about it.

conclusion

The event cycle in the browser is mainly composed of three stages: Task, Microtask and Render. Task and Microtask are mostly used by us. Whether it is network request, DOM operation or Promise, they are roughly divided into these two types of tasks. Each round of the event loop will check whether there are any tasks to be executed in the two task queues. When the JavaScript context stack is empty, all tasks in the microtask queue will be checked first, and then the macro task will be executed. Render is not required, because it is affected by some factors of the browser, and not necessarily executed in each event loop.

Reference

  • Yu – jack. Making. IO / 2020/02/03 /…
  • www.html5rocks.com/zh/tutorial…
  • Html.spec.whatwg.org/multipage/w…
  • zhuanlan.zhihu.com/p/34229323