This is the 13th day of my participation in the August Gwen Challenge. For details, see: August Gwen Challenge juejin.cn/post/698796…

introduce

EventLoop operates on both the browser side and the Node side. Although the mechanisms are different, they both take advantage of the single-threaded and non-blocking features of THE JS language.

EventLoop process

JS performs all operations on a single thread. Although it is a single thread, it has the feeling of multi-threading. In fact, it achieves this effect by using some reasonable data structures.

  • The Call stack is responsible for all the various code to execute

    • When each function completes, it pops off the stack; There’s some other code that needs to be pushed in.
  • The event queue is responsible for sending new functions to the queue for processing

    • Follow the data structure characteristics of queues: first in, first out. Send operations sequentially for execution.
  • Each time an asynchronous function in the event queue is called, it is sent to the browser API.

    • Based on the commands received from the call stack, the API begins its own single-threaded operation.
    • For example, setTimeout, when a setTimeout operation is processed on the stack, it is sent to the appropriate API, which waits until a specified time to send the operation back to the event queue for processing. Thus you have a circular system for asynchronous operations.
    • Loop system: The stack handles setTimout operations and passes them to the corresponding API. The API waits until a specified time to send the operation back to the event queue. The time queue checks if the stack is empty and pushes the operation forward.
  • JS is a single thread, while the browser API acts as a separate thread.

    • The event loop constantly checks to see if the call stack is empty. If empty, a new function is added to the call stack from the event queue; If not empty, the call to the current function is processed.

EventLoop internal

The asynchronous tasks put in by the Event Queue are internally implemented by two queues.

  • Tasks represented by setTimeout are called macro tasks and are placed in the MacroTask Queue.

    • Script (whole code)
    • setTimeout/setInterval,
    • setImmediate,
    • I/O,
    • UI rendering,
    • event listner
  • The tasks represented by promises are called microtasks and are placed in the Microtask Queue.

    • process.nextTick,
    • Promise,
    • Object.observe,
    • MutationObserver

EventLoop handles the logic

  • The JS engine first retrieves the first task from the macro task queue
  • After the execution, all tasks in the microtask queue are taken out and executed in sequence. If new microtasks are created during this step, perform them as well
  • Pull one from the macro task queue, and when it’s done, pull all from the micro task queue. The loop repeats until both queues run out of tasks.

An Eventloop loop processes one macro task and all the microtasks generated in the loop.

The operation mechanism of macro and micro tasks

Code execution sequence 1

console.log('begin');  // Here is the synchronization code
setTimeout(() = > {
    // Asynchronous code
  console.log('setTimeout')},0);
new Promise((resolve) = > {
    // Here is the synchronization code
  console.log('promise');
  resolve()
}).then(() = > {
    console.log('then1');  // Asynchronous code
  }).then(() = > {
    console.log('then2');   // Asynchronous code
  });
console.log('end');  // Here is the synchronization code

begin
promise
end
then1
then2
setTimout
Copy the code

Macro tasks and micro tasks are executed in a basic order. In an EventLoop, each loop is called a tick. The main sequence of tasks is as follows:

  • The execution stack selects the first macro task to be queued and executes its synchronization code until completion;
  • Check whether there are microtasks, if so, execute until the microtask queue is empty;
  • If you’re on the browser side, you’re basically rendering the page;
  • Start the next loop (tick), executing some asynchronous code in the macro task, such as setTimeout, etc.

Macro task

In the browser environment, macro tasks fall into the following categories:

  • Render events (such as PARSING DOM, calculating layout, drawing)
  • User interaction events (such as mouse clicks, page scrolling, zooming in and out)
  • SetTimeout and setInterval
  • Network request completed, file read/write completed event

Macro tasks meet the basic requirements of daily development, but do not meet the requirements for time precision, such as render events, I/O operations, user interaction events, and so on, can be inserted into the message queue at any time. JS has no control over where tasks are inserted in the queue so it is difficult to control when tasks start executing.

Micro tasks

Is a function that needs to be executed asynchronously, after the execution of the main function and before the execution of the current macro task.

When JS executes a script, V8 creates a global execution context for it and also creates a microtask queue to store the microtasks. Multiple microtasks may be generated during the execution of the current macro task, and the microtask queue cannot be accessed directly through JS.

Microtask generation mode

  • Use MutationObserver to monitor a DOM node or to add or remove child nodes to the node. When a DOM node changes, a microtask is created to record the DOM changes
  • Using promises, microtasks are also generated when calls to promise.resolve () and promise.reject () are made.

Timing of microtask execution

When the JS execution in the current macro task is about to complete, that is, when the JS engine is ready to exit the global execution context and clear the call stack, the JS engine will check the microtask queue in the global context, and then execute the microtasks in the queue in order. If a new microtask is generated during the execution of the microtask, the microtask will be added to the microtask queue for execution until the queue is empty. New microtasks created during the execution of a microtask are not deferred to the next loop, but continue to be executed in the current loop.

  • Microtasks and macro tasks are bound, and each macro task creates its own microtask queue when it executes.
  • The duration of the microtask affects the duration of the current macro task. For example, during the execution of a macro task, 10 microtasks are generated, and the execution time of each microtask is 10ms, so the execution time of these 10 microtasks is 100ms, or it can be said that these 10 microtasks prolong the execution time of the macro task by 100ms.
  • In a macro task, create a macro task for callbacks and a microtask for callbacks, which in any case precedes the macro task. Okay

The MutationObserver listens for DOM changes

There was no support for page listening in the early pages, and polling was the only way to observe DOM changes. For example, use setTimeout/setInterval to periodically check for DOM changes. But there are two problems:

  • If the time interval is set too long, DOM changes do not respond timely.
  • Setting the interval too short can waste useless effort checking the DOM and degrade page performance

The MutationObserver API can be used to monitor DOM changes, including property changes, node additions, content changes, and so on. Between the two tasks, other events may be inserted by the renderer process, affecting the timeliness of the response; When a DOM node changes, the rendering engine encapsulates the change record into a microtask and adds it to the current microtask queue.

Asynchronous + microtask strategy:

  • The performance problem of synchronous operation is solved by asynchrony
  • The problem of real-time performance is solved by micro-task

Code is executed in sequence two

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
async1();
setTimeout(() = > {
  console.log("timeout");
}, 0);
new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("promise2");
});
console.log("script end");

async1 start
async2
promise1
script end
async1 end
promise2
timeout
Copy the code
  • Await will block the following code if it is not an await object, so that it executes the async synchronization code outside the async, and then goes back inside the async, treating the non-promise as the result of an await expression.
  • “Await” will also suspend the code behind async. The synchronization code outside async will be executed first, waiting for the promise to be fulfilled, and then the resolve parameter will be taken as the operation result of the await expression.
  • Change the await form code to Promise. It might be easier to understand.
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
async1();

// The code has been changed to Promise
console.log("async1 start");
new Promise(function (resolve) {
    new Promise(function (resolve) {
        console.log("async2");
        resolve();
    })
    resolve()
}).then(function(){
   console.log("async1 end");
})
Copy the code

Code execution sequence three

setTimeout(function () {
  console.log('6')},0)
console.log('1')
async function async1() {
  console.log('2')
  await async2()
  console.log('5')}async function async2() {
  console.log('3')
}
async1()
console.log('4')

// 1 2 3 4 5 6
Copy the code
  • 6 is the macro task executed in the next event loop
  • Output 1 is synchronized, then async1() is called, and output 2.
  • Await async2() will run async2() and 5 will enter the wait state.
  • Output 3, in which case the async code outside the async function is executed to output 4.
  • Finally await the result of waiting and continue with output 5.
  • Enter the second event cycle and output 6.