JS event loop, simply speaking, is written JS code execution order of a loop mechanism.

Event loops in the browser

JS in the browser is mainly the JS engine thread running in the renderer process. (THE JS engine is actually a JS virtual machine, similar to the function of the JVM.)


In addition to the JS engine thread, there are other threads in the rendering process, which are responsible for the rendering display and interaction of the entire page.

There are GUI rendering threads, timer threads, and event handling threads that affect the execution of JS code in the renderer process.


In addition to rendering processes, web processes in the browser can also affect the order in which JS code is executed.

GUI rendering thread

What it does: The GUI rendering thread does HTML, CSS parsing, AND DOM tree building, rendering them to the entire page. Redraw and rearrange utilize this thread as well.

Impact: This thread is mutually exclusive with the JS engine thread. The JS engine thread is suspended while the GUI rendering thread is executing. Conversely, the GUI thread is suspended while the JS engine thread executes. The reason for this is that if the JS code changes the DOM during rendering, which one should it use? The view is out of sync. Similarly, JS was originally designed with single threads in mind.

Conclusion: The rendering process is not executing JS code, and when executing JS code, it is not rendering.

JS prevents DOM parsing. The link CSS file has JS code and will wait for link’s CSS to load before executing the JS code.

In addition to the GUI rendering thread directly blocking the execution of JS code, other parts of the impact of JS code execution follow the event loop.

Browser event loop

The JS interpreter creates a call stack for the execution of JS functions. Each time a function is executed, a memory space is created for the current function to store local variables and execute statements immediately. If the calling function calls another function, then the corresponding space is created in turn and added to the call stack. Once the function call completes, the stack space created by the call stack is destroyed automatically. This is represented by pushing and pushing.

  • Example 1:
function a() {
  console.log("a")
}

function b() {
 console.log("b")
  a()
}

b();
Copy the code
  1. (b) into the stack. Create the corresponding temporary stack space
  2. Console. log(“b”) stacks, executes, and unstacks.
  1. (a) into the stack. Create the corresponding temporary stack space.
  2. Console. log(“a”) stacks, executes, and unstacks.
  1. (a) out of the stack.
  2. (b) out of the stack.

The above examples are all synchronous. But this is how the JS engine calls the main stack code.


All asynchrony in JS is achieved by using other threads or processes. For example, asynchronous requests make use of network processes; Timers are used by timer threads and so on.


  • Through an example to illustrate the asynchronous, JS execution process.
function a() {
  console.log("a");
}

setTimeout(() => {
  console.log("setTimeout");
}, 200);

Promise.resolve().then(() => {
  console.log("promise");
});

axios.get(xxx).then(() => {
  console.log("axios");
});

a();
console.log("over");
Copy the code

  1. Timer setTimeout Hands to the timer thread. Wait 200ms to join the macro queue for execution. The process of waiting does not affect the post-sequence code because the responsible timing is not a thread anymore.
  2. Promsie executes, then executes, and the callback inside is handed over to the microqueue task to be executed.
  1. Axios.get () is pushed into the stack and handed over to the network process. After the network process finishes processing, the task is added to the macro queue. Return the Promise object, then execute. Callbacks to THEN are not added to the microqueue (because the network request has not yet completed).
  2. (a) into the stack. Create the corresponding stack space. Execute synchronization code, console.log(“a”) on the stack, execute, unstack.
  1. Console. log(“over”) pushes, executes, and unpushes.
  2. The call stack is empty
  1. There is an Event Loop polling process that keeps watching to see if the current call stack is empty. If it is empty, it executes what was previously added to the microqueue and macro queue. The sequence is to execute the microqueue first, then the macro queue.
  2. The function in console.log(“promise”) pushes, executes, and pushes out.
  1. At this point, whether an AXIos callback or setTimout is executed depends on the network.
  2. Let’s say setTimout is executed first. (If axios returns first, print Axios first, then setTimeout)
  1. The setTimout callback is pushed onto the stack, executed (console.log(“setTimeout”)), and removed.
  2. Then, the network request comes back, the associated callback is added to the macro queue, the stack is empty, and the macro queue is executed, changing the promise state. Then callbacks are added to the microtask queue.

  1. There is one more task in the microqueue. Push, execute (console.log(“axios”)), push out.
  2. The execution is complete.

The loop above is an event loop that adds execution code to the call stack and tasks to the macro or microqueue.


We can solve some questions:

  1. Why setTimeout is incorrect.
    1. Because setTimeou essentially adds a wait time to the timing thread without executing the code. When the time is up, add the callback task to the macro queue, add it to the call stack, and execute the code. At this point, if the setTiemout code takes too long, it will definitely exceed the set time. If the time set is too short, the process of adding to the queue, adding to the execution stack, may take longer than the set time and may not be accurate. There will be at least a minimum time.
  1. Promsie adds the corresponding callback to the microqueue only when the state changes.
  2. There may also be code in the macro queue that is added to the microqueue. The same is true for microqueues.

Review:

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


setTimeout(function() {
    console.log('6');
    new Promise(function(resolve) {
        console.log('8');
        resolve();
    }).then(function() {
        console.log('9')
    })
})

async1();

new Promise(function(resolve) {
    console.log('10');
    resolve();
}).then(function() {
    console.log('11');
});
console.log('12');
Copy the code

Node side event loop

\

Node is based on Chrome’s V8 engine. The basic event loop flows much like a browser.

Each node event loop goes through the above six phases. The main ones are timers, poll and check.

Pending Callback: This phase performs callbacks for certain system operations, such as TCP error types. For example, some * NIx systems want to wait to report an error if a TCP socket receives ECONNREFUSED while trying to connect. This will be queued for execution in the pending callback phase.

Idle Prepare: schedules the system. Execution is system dependent. Windows, Linux, etc.

Close callbacks: some closed callbacks, such as socket.on(‘close’,…) .

These three phases, heavily influenced by the outside world and the system, are not particularly strongly related to our own code. We just need to pay a little attention to the special ones.

A queue is maintained for each phase. So, there are at least six queues. This is not quite the same as having only two microqueues and macro queues on the browser side.

Each queue contains callback functions, and when all callback functions in the queue are completed, the next stage is entered.

Between each running event loop, Node.js checks to see if it is waiting for any asynchronous I/O or timer, and shuts it down completely if it isn’t.

Timers stage: Callback function for holding timers.

Poll phase: Polling the queue. Except for callbacks in the timers and Checks stages, most callbacks are placed in this queue. For example: file reading, listening to user requests. I’m going to go into this queue.

    • How it works: If there are callbacks in the poll queue, the callbacks are executed in sequence until the queue is empty. If there are no callbacks in poll, wait for callbacks in other queues to end the phase and proceed to the next phase. If no other queue has a callback, wait until a callback occurs.
      • Note: To prevent the polling phase from starving the event loop, Libuv (the C library that implements node.js event loops and all of the platform’s asynchronous behavior) also has a hard maximum (system dependent) before stopping the polling to get more events.
    • Here is an example: The output time of Hello Word is much larger than 200ms, or at least more than 300ms.
const start = Date.now(); setTimeout(function f1() { console.log("hello word", Date.now() - start); }, 200); const fs = require("fs"); fs.readFile(".. HTML ", "UTF-8 ", (err, data) => {console.log("readFile"); const start = Date.now(); while (Date.now() - start < 300) {} });Copy the code

The above example illustrates:

  1. First, after the code execution of the main call stack is complete, there is a corresponding context.
  2. It then determines whether there are currently any events loop related actions that need to be taken. Found timing thread and file read asynchronous callback to execute. Enter the flow of the event loop.
  1. Enter timers, at which time the timer has not yet arrived and callbacks have not yet been added to the timers queue to proceed to the next stage.
  2. The middle is temporarily omitted to enter the poll phase, in which the poll phase execution rules are followed. There are no callbacks to execute in the entire event loop, so it’s stuck in this phase, polling all the time.
  1. At this point, the file read fetch call is added to the poll queue, performing a callback to the associated poll queue, where it is stuck for 300ms. During this process, corresponding queues are also added to Timers.
  2. After the callback to read the file is complete, the poll phase finds callbacks in other queues, and the next phase, check, completes an event loop, and the Event loop, finds any outstanding callbacks, enters the event loop.
  1. The timers phase is displayed, and the callback to empty timers is performed.
  2. Then poll, at which point all the asynchrony is done. Automatically proceed to the next phase until the entire loop is complete and the event loop ends.

Check phase: The callback using setImmediate goes directly to this queue.

    • In the real low-level, in Timers it is the time to check the timer thread. The appropriate callback is executed when the time is up. It’s not really a queue, but it’s ok to think of it as a queue.
    • The Check phase, in which setImmediate is true, is a queue. Using setImmediate, we simply place its callbacks on the queue maintained by the Check phase. It’s much more efficient. The reason is that the Timers phase checks all the timer threads at a time and pulls them out one by one to see if the time is up. That’s a lot slower.
  • First example:
let i = 0;
console.time();
function test() {
  i++;
  if (i < 1000) {
    setTimeout(test, 0);
    // setImmediate(test);
  } else {
    console.timeEnd();
  }
}

test();
Copy the code

Using setTimeout, it takes more than 1s to compare the time with the timer thread each time, which is much slower. With setImmediate, the time would be 20 milliseconds. Because there is no comparison, there is no synchronization with other threads.

  • Second example:
setTimeout(() => {
  console.log("setTimeout");
}, 0);

setImmediate(() => {
  console.log("setImmediate");
});
Copy the code

The above two print cases are not necessarily, because setTimeout is definitely not 0, at least more than 1ms. SetImmediate Mediate Does not perform setTimeout until the second loop of the event loop. If the timer phase has callbacks to the timers phase when the event poll is reached, however, the timer phase does not. SetImmediate does not perform setTimeout until the second loop of the event loop


  • The third example
const fs = require("fs"); fs.readFile(".. HTML ", (err, data) => {setTimeout(() => {console.log("1")) setImmediate(() => console.log("3")); }, 0); setImmediate(() => console.log("2")); });Copy the code
  1. Entering the event loop, the Timers phase with no executable callbacks is reachedpollPhase.
  2. pollPhase finds that the other phases have no callbacks to execute and that events are still waiting. Then inpollPhase continuously polling and waiting.
  1. Execute the appropriate callback until the file has been read. Then, start the timer thread and put thesetImmediateThe callback is added tocheckIn the queue,pollPhase ends.
  2. Enter thecheckPhase, performing the appropriate callback. Type 2.
  1. Then the event loop ends and the callback is not executedtimersPhase. Print 1.
  2. Then go to thecheck, print 3.
  1. All events complete.

NextTick and Promsie

\

All the callbacks that you just performed in the event loop are equivalent to the macro queue tasks represented in the browser.

NextTick and Promise are essentially microqueues.

There are callback cases, priority is nextTick.

The nextTick and PROMISE queues must be emptied each time a callback is intended to be executed in the event loop.

  • The first example
setImmediate(() => {
  console.log(1);
});

process.nextTick(() => {
  console.log(2);
  process.nextTick(() => {
    console.log(6);
  });
});

console.log(3);

Promise.resolve().then(() => {
  console.log(4);
  process.nextTick(() => {
    console.log(5);
  });
});
Copy the code
  1. Type 3.
  2. Enter the event queue and first empty nextTick and Promise. Type 2. Add another one to nextTick, print 6.
  1. Then promsie executes, prints 4, and nextTick comes up again, prints 5.
  2. And then finally print 1.

So, the fastest asynchronous callback you can execute is nextTick. Any asynchronous callback should be executed before it is executed.

\

  • Second example
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}

async function async2() {
  console.log("async2");
}

console.log("script start");

setTimeout(() => {
  console.log("setTimeout0");
}, 0);

setTimeout(() => {
  console.log("setTimeout3");
}, 3);

setImmediate(() => {
  console.log("setImmediate");
});

process.nextTick(() => {
  console.log("nextTick");
});

async1();

new Promise((resolve) => {
  console.log("promise1");
  resolve();
});
Copy the code
  1. printconsole.log("script start")
  2. Print, console.log(“async1 start”), await async2(), print async2. At this point, the promsie returned by Async2 changes its status and joins the promsie microqueue.
  1. Execute the code for new Promise() and print console.log(“promise1”). Then all the synchronization code is executed.
  2. Perform nextTick. printnextTick.
  1. Execute the promsie callback to print async1 end
  2. It then enters the event loop queue.
  1. The timers stage is displayed. There is no telling whether 0ms or 3ms has arrived. So the sequence here and setImmediate is uncertain.
    1. Depends on how long the code takes to execute before entering the event loop.

Why are there so many different asynchronies?

The reason: Initially Node wants to provide an API that executes something immediately during an event loop, so setImmediate is provided. However, it was found later that problems in the waiting process of some poll stages could not be solved. As follows:

Noting that the browser has a microqueue, Node tries to create one, but setImmediate is not used and cannot be changed. Instead, it uses the name nextTick.

Why use process.nexttick ()?

There are two main reasons:

  1. Allows users to handle errors, clean up any unwanted resources, or retry requests before the event loop continues.
  2. It is sometimes necessary to have the callback run after the stack has been unwound, but before the event loop continues. (Examples below)

Here is a simple example of what users expect:

const server = net.createServer(); 
server.on('connection', (conn) => { });

server.listen(8080); 
server.on('listening', () => { }); 
Copy the code

\

Suppose listen() runs at the start of the event loop, but the listening callback is placed in setImmediate(). It is not immediately bound to a port unless the host name has been passed. In order for the event loop to continue, it must hit the polling phase, which means it is possible that a connection was received and the connection event was fired before it could be listened for.

\

conclusion

  • The browser-side event loop is simpler than the Node side.
  • The most important phase of the Node side of the event loop ispollPhase, which acts as a polling phase and listens for all of the Node’s event loop phases to execute corresponding callbacks. To take the next step. In network requests, extra is important. Acts as a node for network request and subsequent code execution.