preface

It seems that since 2018, event loops have become popular in front end interviews, and now they are the basis of the top end interview questions. Understanding and learning event loops is not only a basic skill for programmers, but also a must-have interview question. Although the cycle of events is not “new” and there are many articles about it on the Internet, there are “a thousand Hamlets in a thousand people’s eyes” and everyone has their own interpretation of it. Therefore, I also wrote down what I understood about the browser event loop and how I understood it.

Processes and threads

Before we get into the main body, we need to review the basics: processes and threads. Process is the most basic unit of resource allocation, the basic unit of operation scheduling, and has its own independent address space and resources. Threads are the smallest unit of operations performed in a process, and each process contains at least one thread.

This may be confusing, but why do we need to know about processes and threads? This is because every browser page is opened a process is started (the system allocates resources to the browser: memory, etc.), and the browser event loop runs in the page’s rendering thread. We can conduct more intuitive cognition through the browser of the browser, including auxiliary processes such as GPU process and NetWork.

Browser resident thread

  • GUI rendering thread: the main functions are to draw pages (page redraw and backflow), parse HTML and CSS, build DOM trees, layout and draw, etc. This thread is mutually exclusive with the JavaScript engine, so called JS performs blocking page updates.

  • JS engine thread (main thread) : Responsible for the execution of JavaScript script, and GUI rendering thread is mutually exclusive, execution time is too long will block page rendering.

  • Event trigger thread: responsible for handing the prepared events to the JS engine thread for execution. Because JavaScript is single threaded, it needs to queue up when multiple events are added to the task queue.

  • Timer trigger thread: it is responsible for executing asynchronous timer events, such as setTimeout and setInterval. When the timer runs out, the registered callback is added to the end of the queue. (The timer is not timed by the JS engine, because the JS engine is single threaded, and if the JS engine is blocked, the timing accuracy will be affected. Therefore, when timing completion is triggered, events are added to the event queue, waiting for the JS engine to be free to execute).

  • HTTP request thread: responsible for the execution of asynchronous request, JS engine thread will execute the asynchronous request when the function to the thread processing, when listening to the state change event, if there is a callback function, the thread will add the callback function to the end of the task queue waiting for execution.

Why are JS engines single threaded? Because JavaScript can operate on the DOM tree, if there are multiple threads, the JavaScript code can be modified or deleted while rendering the DOM, which will result in rendering exceptions. Therefore, to prevent this phenomenon, GUI rendering thread and JS thread are designed to be mutually exclusive. When the JS engine executes, the GUI thread needs to be frozen, but the GUI rendering is stored in a queue, waiting for the JS engine to execute the rendering.

Why do browsers need event loops?

Browsers not only perform internal tasks, but also trigger events such as user input (events trigger threads). So how to make the user triggered tasks and internal tasks reasonably orderly execution? This can be concatenated by the previous thread:

The render main thread is used by an IO thread to receive messages from other processes, such as resources fetched by HTTP request threads and event tasks triggered by event-triggering threads. The IO thread is essentially a message queue, conforming to the “first in, first out” nature of the queue, and in order for the render main thread to render the updated page according to each task, there needs to be a loop event for the message queue to read the task and execute it, and this loop event is the browser event loop.

Macro and micro tasks

How does the inside of the browser event loop actually work? We can look at it from the perspective of code execution:

Event loops are task-driven and are divided into synchronous and asynchronous tasks, while asynchronous tasks are divided into macro tasks and micro tasks (there is no such thing as macro tasks; macro tasks are a concept coined to distinguish between micro tasks).

In JSC engine terminology, tasks initiated by the host (Node, browser) are called macro tasks, and tasks initiated by the JavaScript engine are called micro tasks

. Microtasks are smaller tasks than macro tasks that enable us to perform the specified behavior before rerendering the UI, avoiding unnecessary UI redrawing and making the application state more continuous. Microtasks have two execution opportunities:

1. When executing the microtask checkpoint in event-loop (the existence of the microtask checkpoint is to prevent the scenario where the macro task is empty but the microtask is not empty)

2. Microtasks need to be executed asynchronously as soon as any script task is finished.

Why do microtasks exist?

Causes of microtasks: If synchronous notification is adopted when DOM changes, the execution efficiency of current tasks will be affected. If the asynchronous notification is used, the real-time monitoring will be affected, because in the process of adding to the message queue, there may be a lot of tasks in front of the queue, so the micro-task application is born.

Macro task, micro task relationship

What is the relationship between the two? Macro tasks are the norm. When JavaScript is executed, a macro task is initiated, and an instruction is executed in the macro task. There can be more than one macro task at a time, but because the tasks are stored in a queue, they are executed one by one in order, in accordance with the “first-in, first-out” nature of the queue. Each macro task can be followed by a microtask queue. If there are instructions or methods in the microtask queue, the next macro task will be executed after the current microtask is executed. If not, the next microtask is performed. Therefore, the execution duration of the microtask will affect the duration of the current macro task.

Common scenarios of macro and micro tasks

There are several common macro tasks:

  • Render events (such as PARSING DOM, calculating layout, drawing, etc.)

  • User interaction events (such as mouse clicks, page scrolling, zooming in and out, etc.)

  • JavaScript scripts execute events

  • Network request completion, file read and write completion events, etc

Common microtasks include: Promise’s callback function, MutationObserver, etc.

With that said, we can test this theory with a bit of code. Let’s first look at the output order of setTimeout, Promise, and normal code:

function sleep(duration) { return new Promise(function(resolve, reject) { console.log("b"); // New Promise() is a synchronous method, resolve is an asynchronous method, execute order 3 setTimeout(resolve,duration); }) } console.log("a"); Sleep (1000). Then (()=>console.log("c", new Date()))); // Microtask 1, execution sequence 4}, 0); console.log("d"); // Macro task 3, execution sequence 2Copy the code

The output is as follows:

Contrary to what we might expect, setTimeout is also a macro task, but it is the last macro task to execute. This is because setTimeout is a timer task and will be executed after other macro tasks have completed.

Async/Await

The basic concept

In ES7, JavaScript introduced async/await. Async is a function that executes asynchronously and implicitly returns a Promise as a result. Multiple promises can be used with await, and multiple nesting of multiple promises is supported.

We can say async/await provides the ability to access resources asynchronously using synchronous code without blocking the main thread and makes the code logic clearer. You can refer to the following code:

async function foo(){
   return 'hello';
}
console.log(foo()); // Promise {<fulfilled>: 'hello'}
Copy the code

Await the expression

In the MDN official documentation, the await operator is used to wait for a Promise object. It can only be used in async function. The return value is the result of processing the Promise object; If you are waiting for something other than a Promise object, the value itself is returned.

If we combine our previous knowledge of macro tasks, micro tasks, async/ AWIAT to consider the following code execution order:

async function foo(){ console.log(1); let a = await 100; console.log(a); console.log(2); } console.log(0); foo(); console.log(3); // result: 0, 1, 3,100,2Copy the code

The output may be different from what we expect. What process is causing this difference? We can follow the code first:

First, console.log(0) prints 0;

Second, we go to testFunc, console.log(1), print 1; As we understood earlier, await 100 returns 100, so console.log(a) should be called and 100 printed, but console.log(3) should be called first. What is the reason for this?

We need to know two more things here: generator functions and coroutines.

A Generator function is an asterisked function that pauses and resumes execution (those of you who have used Redux-Saga are probably familiar with this).

Get familiar with the code:

function * foo(){
  console.log(1);
  yield 'generator 1';
  
  console.log(2);
  yield 'generator 2';
  
  console.log(3);
  return 'generator 2';
}
console.log(0);
let gen = foo();
console.log('outside 1');
console.log(gen.next().value);
console.log('outside 2');
console.log(gen.next().value);
console.log('outside 3');
console.log(gen.next().value);
Copy the code

The result is as follows:

From the output, we can see that function foo is not executed all at once. The global code and function foo are always executed alternately. That is, if you execute a piece of code inside a generator function, if the yield keyword is encountered, the JS engine will return the content after the yield keyword to the outside and suspend the function, and the outside function can be executed using the next method. So how does the internal engine do function pause and resume? This is where the concept of coroutines comes in. Coroutines can be said to be tasks being performed on a thread. Coroutines are not controlled by the operating system, but are completely controlled by the program. A thread can have multiple coroutines, but a thread can only execute one. For example, if you are executing A coroutine, if you need to execute B coroutine, A coroutine needs to give control of the main thread to B coroutine (if A coroutine starts B coroutine, A is called the parent coroutine of B).

If we look at the previous example, we can see that our code starts a foo coroutine because foo is flagged async, and our await 100 equals:

let promise_ = new Promise(resolve, reject) => {
  resolve(100);
});
Copy the code

The overall flow is pipelined as follows, return a promise_ to the parent coroutine in await 100, which is equivalent to adding a microtask to the parent coroutine microtask queue.

conclusion

The above is my personal understanding of the browser event cycle, in fact, we can regard the entire operation of our browser as a factory assembly line, and each thread, task is just a tool on the line, understand the basic process, you can be more confident about the running results of the code.