preface

Recently, I have been reading the React source code, and found that updates in React concurrent mode use asynchronous rendering. I happened to want to summarize an article about Event loop to consolidate my understanding of event loop.

About the browser process

Browsers are multi-process, and each TAB page is a separate browser process.

The browser also includes a tool similar to Task Manager, which can be opened in Chrome from the menu -> More Tools -> Task Manager.

As you can see from the image, each TAB page is a separate browser process.

Processes included in the browser:

As you can see, the browser mainly consists of four processes:

  1. Browser process: The main process of the browser, responsible for coordinating and controlling the browser, only one.
    • Responsible for browser interface display and user interaction, such as forward, backward, etc
    • Responsible for page management, creating and destroying other processes
    • Draw an in-memory image from the Renderer process onto the user interface
    • Network resource management, download, etc
  2. Third-party plug-in process: a process created when a third-party plug-in is used.
  3. GPU process: a maximum of one, responsible for 3D image drawing.
  4. Rendering process: that is, the process with the closest front-end contact, JS execution, interface rendering and so on are executed in this process.

Rendering process

As you can see in the image above, the rendering process is multi-threaded:

  1. The GUI thread:
    • Only one, responsible for rendering the page, parsing HTML, CSS building the Render tree, layout and drawing, etc
    • This thread executes when the page is redrawn or restreamed
    • GUI thread and JS engine thread are mutually exclusive. When the JS engine thread is executing, the GUI thread will be suspended, and the GUI update task will be saved in a queue. When the JS engine thread finishes executing all tasks, it will take out the update task from the queue to execute.
  2. Js engine thread:
    • Also known as the JS kernel, responsible for parsing and executing JS scripts, such as v8 engines.
    • There is only one JS engine thread in a Tab page, which is often referred to as a single JS thread.
    • GUI thread and JS engine thread are mutually exclusive. When the JS engine thread is executing, the GUI thread will be suspended, and the GUI update task will be saved in a queue. When the JS engine thread finishes executing all tasks, it will take out the update task from the queue to execute.
  3. Timer thread:
    • That’s the thread where setTimeout and setInterval are
    • The browser timer is not counted by the JS engine, because the JS engine is single-threaded. If a large number of tasks are executed before the timer, you need to wait for the completion of all previous tasks before starting the timer, which will result in inaccurate timing.
    • When the timer is finished, the callback event will not be executed, but will be added to the task queue of the event cycle, waiting for the JS engine idle to take out the execution of the task queue.
  4. Event trigger thread:
    • The event-triggering thread belongs to the browser, not the JS engine. The JS engine processes too many transactions and needs the browser to open the thread for assistance, mainly responsible for controlling the event cycle
    • JS adopts event-driven mechanism to respond to user operations. Event threads respond and process events by maintaining event cycles and event queues.
    • When processing asynchronous code, such as declaring a click event, when the user clicks, the click event will be added to the end of the event queue, such as js engine idle, it will be removed from the event queue execution.
  5. HTTP request thread:
    • There are multiple
    • Start a new thread for the request by creating an XMLHttpRequest instance.
    • When a state change is detected, if a callback function is set, the asynchronous HTTP request thread will generate a state change event and put the callback function into the event queue, waiting for the JS engine to be idle.

Js call stack

In JS, when a function is executed, an execution context will be generated, which contains the parameters and variables of the function. Any javascript code is run in the execution context, and then the execution context will be pushed into the call stack, which can also be called the execution context stack. When the function completes, the execution context of the function is removed from the call stack.

Js is a single-threaded language, which means it has only one call stack, so JS can only do one thing at a time.

Here’s an example:

function funA() {
  console.log('A');
}

function funB() {
  funA();
}

funB();
Copy the code

Executing this code produces the following steps:

Note: Execution context is generated only when the function is called, and a new execution context is generated with each call.

  1. The first step is to call funB, generate an execution context and push it onto the call stack.
  2. Start executing funB, enter funB and encounter a funA function, which generates the execution context of funA and pushes it onto the call stack.
  3. FunA starts executing, funA encounters console.log, and the execution context of console.log is generated and pushed onto the stack.
  4. Log is executed, and after execution, the execution context of console.log is removed from the call stack.
  5. There is no more code to execute in funA. After execution, the funA execution context is removed from the call stack.
  6. There is no more code to execute in funB. After execution, the funB execution context is removed from the call stack.

Whether there is an execution context in the call stack indicates whether the JS engine is executing, that is, whether it is idle.

Event Loop

As mentioned above, JS is executed on the JS engine thread and can only do one thing at a time. If there are some particularly time-consuming operations, such as network requests, I/O, etc., it is very inefficient to wait for the completion of the previous task before doing the next task. In order to solve this problem, an event loop is created.

Event loops are a mechanism for asynchronous event execution. Different threads can communicate with each other through a common area called the event queue, also known as the task queue.

When executing JS code, some code is executed synchronously and some is executed asynchronously.

Such as:

console.log('hello'); // Synchronize the code, and upon execution of this code, the console prints hello

setTimeout(function() {
  console.log('setTimeout');
}, 1000); // The asynchronous code will be printed to the console after 1 second: setTimeout
Copy the code

So how does JS execute when encountering asynchronous code?

When the asynchronous code is encountered, the asynchronous event is first added to the event queue. When all the events in the call stack are executed, the event loop will take the event from the event queue and put it into the call stack to start execution. This process is cyclic, so it is called event loop.

There are two main functions of the event loop:

  1. Listen on the call stack to check if it is empty
  2. When the call stack is empty, the event is fetched from the event queue and executed in the call stack

The timer

In fact, timer is not asynchronous, but when the timer is called, such as setTimeout, the timer thread will be called to start the countdown. When the countdown ends, the timer thread will put the callback function into the event queue. When the call stack is empty, the timer callback function will be executed. If at the end of the countdown, there are many tasks in the event queue or call stack that are not executed, then the timer callback function will not execute immediately, and the accuracy of the timer timing cannot be guaranteed.

Macrotasks and Microtasks

In asynchronous code, there are macro tasks and micro tasks, so there are task queues of corresponding tasks in the event loop: macro task queues and micro task queues.

Macro tasks can be understood as code executed each time in the execution stack, and the browser renders the page after one macro task ends and before the next macro task starts executing. Macro tasks include:

  • Script (can be understood as synchronous code)
  • setTimeout/setInterval
  • UI interaction events
  • The UI rendering
  • I/O
  • postMassage
  • MassageChannel
  • setImmediate

Microtasks can be understood as tasks performed before page rendering after the current macro task is completed. Microtasks include:

  • Promise
  • Object.observe
  • MutaionObserver
  • process.nextTick

Execution sequence of macro task and micro task:

  1. The synchronization code will be executed first, and the task will be put into the microtask queue when it meets the microtask. All the microtasks encountered in this event cycle will be executed in this cycle. If the macro task is encountered, it will be added to the macro task queue and executed in the next event cycle.
  2. Check whether there are tasks in the microtask queue. If there are, the microtasks will be taken out and put into the call stack for execution until the microtask queue is empty.
  3. When the microtask is complete, the browser checks for page updates and renders the page if any.
  4. After rendering, check if there are any tasks in the macro task queue, and pull one out and put it into the call stack for execution
  5. After the removed macro task is completed, the loop is executed again from Step 2.

example

Let’s verify this with code:

console.log('1 = = = = = = = = = = =');

// setTimeout1
setTimeout(function timeout1() {
  console.log('2 = = = = = = = = = = =');
  new Promise(function promiseTest(resolve) {
    console.log('3 = = = = = = = = = = =');
    resolve();
  }).then(function promiseThenTest() {
    console.log('4 = = = = = = = = = = =');
  });
}, 0);

// setTimeout2
setTimeout(function timeout2() {
  console.log('5 = = = = = = = = = = =');
}, 0);

new Promise(function promiseTest(resolve) {
  console.log('6 = = = = = = = = = = =');
  resolve();
}).then(function promiseThenTest() {
  console.log('7 = = = = = = = = = = =');
});

console.log('8 = = = = = = = = = = =');
        
Copy the code

The execution result is as follows:

1= = = = = = = = = = =6= = = = = = = = = = =8= = = = = = = = = = =7= = = = = = = = = = =2= = = = = = = = = = =3= = = = = = = = = = =4= = = = = = = = = = =5= = = = = = = = = = =Copy the code

Let’s break it down:

  1. First execute code encounterconsole.log('1===========');, this code is synchronous code, so after execution the console prints:1 = = = = = = = = = = =
  2. And then I go down and I hitsetTimeout1.setTimeoutIs a macro task, put into the macro task queue
  3. And then we go down and we meetsetTimeout2, also into the macro task queue
  4. Continue down, encounteredPromise.PromiseThe instantiated callback is synchronous code, so after execution the console prints:6 = = = = = = = = = = =.thenMethods are placed in the microtask queue
  5. Finally meetconsole.log('8===========');, this code is synchronous code, so after execution the console prints:8 = = = = = = = = = = =. Here we go. Sync code is done
  6. And then you start pulling the task execution from the microtask queue, that is, will executePromise.thenMethod, the console prints:7 = = = = = = = = = = =
  7. At this point, there are no more microtasks, and the browser decides if it needs to render the interface, completes it, and proceeds to the next event loop
  8. Fetch from the macro task queuesetTimeout1, executes the callback function, encounteredconsole.log('2===========');, the console prints:2 = = = = = = = = = = =And then encounteredPromise.PromiseThe instantiated callback is synchronous code, so after execution the console prints:3 = = = = = = = = = = =.thenMethod into the microtask queue.
  9. At this point,setTimeout1After the execution of the callback function, that is, the macro task is finished, it will go to the micro task queue to take out the task execution, that is, in the previous stepsetTimeout1In thePromise.thenMethod, the console prints:4 = = = = = = = = = = =
  10. setTimeout1In thePromise.thenAfter the method is executed, the microtask queue is empty and the browser does not need to render, so the next event loop is directly opened.
  11. Pull it from the macro task queuesetTimeout2, execute the callback function, and the console prints:5 = = = = = = = = = = =

Note: If a new macro task is encountered during an event loop, it will not be executed until the next event loop, such as setTimeout1 and setTimeout2 in the example above.

Let’s take another example to verify that in an event loop, if a new macro task is encountered, it will not be executed until the next event loop:

    
  <div id="outer" style="width: 100px; height: 100px; background: yellow;">
      <div id="inner" style="width: 100px; height: 100px; background: red;"/>
  </div>
    
  var inner = document.getElementById('inner');
  var outer = document.getElementById('outer');

  outer.onclick = function () {
    console.log('outer=========');
    setTimeout(() = > {
      console.log('outer=========setTimeout');
    }, 0);

    new Promise(function outerPromise(resolve) {
      console.log('outerPromise===========');
      resolve();
    }).then(function promiseThenTest() {
      console.log('outerPromise===========then');
    });
  }

  inner.onclick = function () {

    console.log('1 = = = = = = = = = = =');

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

    new Promise(function promiseTest(resolve) {
      console.log('3 = = = = = = = = = = =');
      resolve();
    }).then(function promiseThenTest() {
      console.log('4 = = = = = = = = = = =');
    });

    console.log('5 = = = = = = = = = = =');

  };
Copy the code

This is an example of an event bubbling up and the end result is:

1= = = = = = = = = = =3= = = = = = = = = = =5= = = = = = = = = = =4===========
outer=========
outerPromise===========
outerPromise===========then
2===========
outer=========setTimeout
Copy the code

Let’s analyze it in detail:

  1. After clicking on the inner element, an event bubble occurs, and the event-triggering thread in turn adds the click event to the macro task queue.
  2. Then take out the inner element’s click event execution, start execution, then encounter setTimeout (macro task) and Promise (micro task), put into their respective task queue, execute the synchronization code and print: 1,3,5, after the click event execution (macro task), execute the micro task and print: 4.
  3. When the microtask is cleared and the browser does not need to render, the next event loop is performed.
  4. Get outer’s click event execution from the macro task queue, start execution, then encounter setTimeout (macro task) and Promise (micro task), put into their respective task queues, execute synchronization code and print:The outer = = = = = = = = =, outerPromise = = = = = = = = = = =, click the completion of the event (macro task), then execute the micro task and print:outerPromise===========then.
  5. When the microtask is cleared and the browser does not need to render, the next event loop is performed.
  6. According to the order above, the first macro task, setTimeout in the inner click event, is taken out and executed, printing: 2
  7. After executing the macro task, check whether there is a task in the microtask queue. Now there is no task, and the browser does not need to render, then the next event loop.
  8. Select outer from the macro task queue and click setTimeout in the event to execute, printing:outer=========setTimeout