sources

A previous post by Yang JingZhuo on GitHub titled “Exploring javaScript async and Browser update rendering timing from the Event Loop specification” opened new insights into Event loop. Therefore, this output note is obtained through simplification and modification on the basis of the article.

1. Relationship between asynchrony and EventLoop

First of all, this picture comes to mind when I think of EventLoop. That’s true, but we haven’t figured out how EventLoop handles event polling. How do you handle asynchronous synchronization internally? This problem has to do with macro tasks and micro tasks.

EventLoop is hidden and unfamiliar to many people. But when it comes to asynchrony, I believe everyone knows. The backstop behind asynchrony is EventLoop. The asynchron is actually called a browser EventLoop or javaScript environment EventLoop because ECMAScript does not have EventLoop. EventLoop is defined in HTML Standard.

console.log('start')

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

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

console.log('end')

// start
// end
// promise1
// promise2
// setTimeout
Copy the code

The above sequence is run on Chrome, interestingly tested in Safari 9.1.2, promise1 promise2 is after setTimeout, and safari 10.0.1 yields the same result as Chrome. Understanding tasks, microTasks queues, can answer the question of why browsers behave differently.

2. EventLoop/ macro/micro task definition

2.1 EventLoop definition

EventLoop translates to EventLoop and can be understood as a way to achieve asynchrony,

Events, user interaction, scripting, rendering, networking these are all familiar things that are coordinated by EventLoop. Trigger a click event, make an Ajax request, all with an EventLoop running behind it.

2.2 Task (also known as macroTask)

An EventLoop has one or more task queues.

When the user agent schedules a task, it must be added to a task queue in the corresponding EventLoop.

Each task comes from a specific task source, such as a task queue for mouse and keyboard events, and a separate queue for other events. You can allocate more time for mouse and keyboard events to ensure smooth interaction.

A task queue, also known as a macrotask, is better understood as a first-in, first-out queue in which a task is provided by a specified task source.

What are the task sources?

The specification is mentioned in Generic Task Sources:

DOM manipulation task source: This task source is used for corresponding DOM operations, such as inserting an element into a document in a non-blocking manner.

User interaction task source: This task source is used to respond to user interactions, such as keyboard or mouse input. Events that respond to user actions, such as Click, must use the Task queue.

Network task source: Network task source is used to respond to network activity.

History Traversal task source: The task is inserted into the task queue when a similar API such as history.back() is called.

Task task sources are very broad, such as Ajax onload, click events, basically all kinds of events that we often bind are task task sources, and database operations (IndexedDB), Note that setTimeout, setInterval, and setImmediate are also task sources. To summarize the task source (macroTask) :

  • setTimeout
  • setInterval
  • SetImmediate (unique) Node
  • I/O
  • UI Rendering (browser only)

2.. microtask

Each EventLoop has a MicroTask queue (the biggest difference from MacroTask), and a microtask is placed in a MicroTask queue rather than a task queue.

A MicroTask queue is similar to a task queue in that it is a first-in, first-out queue, and the task is provided by the specified task source. However, an EventLoop has only one MicroTask queue.

The HTML Standard does not specify which microTask task sources are, but it is generally considered that microTask task sources are:

  • Process. NextTick (unique) Node
  • promises
  • Object.observe
  • MutationObserver

NOTE: Promises are defined in the ECMAScript specification, not in the HTML specification, but there is a jobs concept in the ECMAScript specification that is very similar to microtasks. In the Notes 3.1 Promises/A + specification mentioned promise then method can be used in A “macro (macro – task) task” mechanism or “micro task (micro – task)” mechanism to realize. This is why the promise mentioned at the beginning is different in different browsers. Some browsers put then in the Macro-Task queue, while others put it in the micro-task queue. In His blog Tasks, MicroTasks, Queues and Schedules, Jake mentioned a discussion about malling list discussions. There is a general consensus that promises belong to the Microtasks queue.

Note that NodeJs EventLoop works differently from browser EventLoop. The JavaScript environment in NodeJS is V8 and single-threaded, but there are some differences in how it behaves in browsers. The difference between NodeJS and browsers is that there are several types of macro tasks in NodeJS, which have different task queues, and different task queues have different order, while microtasks are interspersed between each type of macro tasks. In the Node environment, process.nextTick has a higher priority than Promise, which can be simply interpreted as that the nextTickQueue part in the microtask queue will be executed first after the macro task ends, and then the Promise part in the microtask will be executed. This article will focus on EventLoop in the browser.

3. EventLoop processing

In general, the EventLoop process can be summarized as follows:

  • The EventLoop loops continuously to fetch the oldest task in the tasks queue and push it to the stack for execution, and then executes and clears the tasks in the MicroTask queue in the next loop.
  • After executing tasks in the MicroTask queue, it is possible to render updates. (Browsers are smart, they don’t respond immediately to multiple DOM changes within a frame, but instead accumulate changes to update the view at up to 60HZ)

3.1 Illustration of macro and micro tasks



Process:

  • First, execute a macro task, and determine whether there are microtasks after execution

  • With microtasks perform all microtasks first and then render, without microtasks render directly (render also needs to refresh the render on a regular basis)

  • Then proceed to the next macro task

A few other points to note:

  • Browsers are multi-process, opening a page is a process.
  • The JS engine is single threaded.
  • Macro/micro tasks are run in the JS engine thread. Browsers have something called GUI rendering threads. The two threads are mutually exclusive, and the GUI thread is suspended when the JS engine executes, and vice versa.
  • GUI rendering threads are responsible for: rendering browser interfaces, parsing HTML, CSS, building DOM trees and RenderObject trees, layout and drawing, etc. When we change the color of some element or the background color, the page is repainted. When we change the size of the element, the page is Reflow. When the page needs to be Repaing or Reflow, the GUI thread executes and draws the page.

3.2 Complete EventLoop procedure

Norms are hard to understand, to make a figurative metaphor:

The main line is similar to a processing plant, which has only one assembly line. The task to be performed is the raw material on the assembly line. Only after the first processing, the last one can be carried out. (JavaScript is single-threaded, with only one execution stack to execute the code.) EventLoop is the worker who puts the raw material (microtask/macro task) onto the pipeline (execution stack). As long as they are already on the pipeline, they are processed in sequence, called synchronous tasks. Some of the raw materials to be processed, workers will be sorted according to their species, at the appropriate time to put on the assembly line, these are called asynchronous tasks.

As a simple example, suppose a script tag has the following code:

<script>
Promise.resolve().then(function promise1 () {
       console.log('111111');
    })
setTimeout(function setTimeout1 (){
    console.log('222222')
    Promise.resolve().then(function  promise2 () {
       console.log('333333'); })},0)

setTimeout(function setTimeout2 (){
   console.log('444444')},0)
</script>
// Output: 111111 -> 222222 -> 333333 -> 444444
Copy the code

Operation process:

The code in script is listed as a task and placed in the Task queue.

Cycle 1:

[Task queue: script; microtask queue: none] Remove the script task from the task queue and push it to the stack for execution. Promise1 is listed as a microtask, setTimeout1 as a task, and setTimeout2 as a task. Task: setTimeout1 setTimeout2; Microtask: Promise1 after a macro task is executed, all microtask queues are emptied. Script After a macro task is executed, run microtask checkpoint to retrieve the promise1 task from the microTask queue. Loop 2:

[Task queue: setTimeout1 setTimeout2; microTask queue: none] Remove setTimeout1 from the task queue and push it onto the stack. 【 Task queue: setTimeout2; MicroTask queue: promise2】 when a macro task is finished, all its microtask queues are emptied. A global task is a macro task. Perform a microtask checkpoint and extract the promise2 of the MicroTask queue. Cycle 3:

[Task queue: setTimeout2; microTask queue: none] Remove setTimeout2 from the task queue and push it to the stack. SetTimeout2 Run the microtask checkpoint command. 【 Task queue: None; MicroTask queue: None 】

3.3 Take a look at the implementation of EventLoop

This example analysis method is the same as the above one, the main point is:Think of global synchronization code as a macro task.

Summary: When the overall script(as the first macro task) starts to execute. After executing a macro task, all current microtask queues are cleared. This is the EventLoop round. Thus the operation of repeating the loop is carried out.

4. The relationship between macro/micro task execution and rendering

  • The same DOM is modified multiple times in an Event loop, drawing only the last time.
  • The rendering is updated after the Tasks and microtasks in the Event loop are completed, but not every turn of the Event loop. This depends on whether the DOM has been modified and whether the browser feels the need to present the new state to the user immediately at this point. If multiple DOM changes are made in the span of a frame (the time is uncertain because the browser’s frames per second always fluctuate, 16.7ms is just not an accurate estimate), it might be reasonable for the browser to stack up the changes and draw only once.
  • If you want to render changes in real time in each round of the Event loop, you can use requestAnimationFrame.

Here are some examples to illustrate the considerations between macro/micro task execution and rendering.

4.1 Rendering Case 1

document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:pink';
Copy the code



We see the above diagram directly apply colours to a drawing the pink background, according to the above telling the browser will be performed in a macro task, then the current execution stack of all tasks, and then handed over to the GUI rendering, the above four lines of code are macro tasks belong to the same time, to perform rendering, completing execution of the rendering GUI thread will merge all UI changes to optimize, so visually, You just see the page turn pink.

4.2 Rendering Case II

document.body.style = 'background:blue';
setTimeout(() = >{
    document.body.style = 'background:pink'
},0)
Copy the code

In the code above, the page will be blue and then a pink background.

The first macro task executes code that turns the background blue, triggers a rendering that turns the page blue, and triggers a second macro task that turns the background pink.

4.3 Rendering Case three

document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(() = >{
    console.log(2);
    document.body.style = 'background:pink'
});
console.log(3);
Copy the code

Console output 1, 3, 2. That’s because the first macro task of the global code starts with parsing output 1, registering promise, and output 3 from top to bottom. But promise.then is asynchronous, which pushes it into the microtask queue. When it terminates the macro task, all microtask queues are cleared. Render again. So you can’t see the blue, you can see the pink.

4.4 Comprehensive Case

function test() {
  console.log(1)
  setTimeout(function() {
    console.log(8)},1000)
}

test()

setTimeout(function() {
  console.log(5)})new Promise(resolve= > {
  console.log(2)
  setTimeout(function() {
    console.log(7)},100)
  resolve()
}).then(function() {
  setTimeout(function() {
    console.log(6)},0)
  console.log(4)})console.log(3)
// The output is 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8
// There is no need to explain.
Copy the code

4.5 Cases with async/await

    console.log('1')
    async function async1() {
      console.log('2')
      await async2()
      console.log('3')}async function async2() {
      console.log('4')
    }

    async1()

    new Promise(function(resolve) {
      console.log('5')
      resolve()
    }).then(function() {
      console.log('6')})console.log('7')
    // Output 1 -> 2 -> 4 -> 5 -> 7 -> 3 -> 6
Copy the code

Await async2() = await async2() = await async2()

// synchronization function
const a = await 'hello world'
/ / equivalent to
const a = await Promise.resolve('hello world');
Copy the code

When the engine encounters “await” keyword, it executes the function immediately following “await” and adds the code immediately following “await” to microTask. So, the function above is as follows:

async function async1() {
	console.log('async1 start');
	await async2();
	console.log('async1 end');
}
/ / equivalent to the
async function async1() {
	console.log('async1 start');
	Promise.resolve(async2()).then(() = > {
                console.log('async1 end'); })}Copy the code

Because async/await is essentially based on some encapsulation of promises, which are a type of microtask. So using the await keyword has the same effect as promise.then. Async code before await is executed synchronously. It can be understood that code before await belongs to code passed in new Promise, and all code after await is callback in promise.then. Async function is essentially a syntactic sugar for generator + Promise +run mode.

reference

Explore javaScript asynchronous and browser update rendering time microtask, macro task and event-loop from the event loop specification