Event loop in JS

Although JS runs asynchronous JS code, in fact, JS itself has no built-in asynchronous concept.

The JS engine does not run in an isolated region. Instead, it runs in a host environment, which could be a browser or Runtime like Node. All of these environments have the same mechanism: Event Loop.

JS events can be divided into two types: macro-task and micro-task.

  • Macro tasks: Script entire code, setTimeout, setInterval, setImmediate, I/O, UI Render
  • Microtasks: Promise.then, process.nectTick(Node), Object.observe(discard), MutationObserver.

Event-loop in the browser

JavaScript has a main thread and a call-stack, where all tasks are placed to wait for the main thread to execute.

JS call stack

The JS call stack uses a lifO (last in, first out) rule. When specified, the function is added to the top of the stack, and when the execution stack is complete, it is removed from the top of the stack until the stack is empty.

Synchronous and asynchronous tasks

JS is divided into single thread task synchronization task and asynchronous tasks, the synchronization task will in the call stack according to the order for the main thread to perform at a time, an asynchronous task will result in the asynchronous task after task will register the callback function in a queue for the main thread idle time (the call stack is empty), are read to wait for the main thread of execution stack.

A task Queue is a typical Queue structure, first in, first out.

To put it simply, the execution flow of a task would look like this:

Process model of event loop

  • Select the current task queue and the first entered task in the task queue. If the task queue is empty, the system jumps to the execution microtask queue
  • Sets the task in the event loop to the selected task
  • Perform a task
  • Sets the current running task in the event loop to NULL
  • Deletes a running completed task from the task queue
  • Microtasks Step: Enter the MincroTask checkpoint
    • Set the MicroTask checkpoint flag to true
    • When the event loop mincroTask execution is not empty:
      • Select a microtask that is most advanced to the microtask queue
      • Sets the microtask for the event loop to the selected microtask
      • Running microtasks
      • Set the executed microtask to NULL
      • Remove the microtask
    • Clean up IndexDB food
    • Set mincroTask checkpoint flag bit false
  • Update interface rendering
  • Go back to step 1

In simple terms, the execution stack checks whether the execution stack is empty after the synchronization Task is executed. If the execution stack is clear, the Task(macro Task) is executed. After each macro Task is executed, the microTask(microTask) is checked whether the microTask is empty. Execute the full microtask on a first-in, first-out basis, then reset the microtask queue, and then execute the macro task. And so on.

A simple example

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')}async function async2() {
  console.log('async2 end') 
}
async1()

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

new Promise(resolve= > {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')})console.log('script end')
Copy the code

Here, the result of the first synchronization task is output:

script start
async2 end
Promise
scriptend
Copy the code

Here are the microtasks pushed into the queue:

async1 end
promise1
promise2
Copy the code

So perform these microtasks. Note that promisE2 is pushed into the current microtask queue during the execution of a microtask.

Finally execute macro task:

setTimeout
Copy the code

Event Loop in Node

The Event Loop in Node is a completely different thing from the Event Loop in the browser. Node.js uses V8 as the PARSING engine of JS, and LIbuv is used for I/O processing. Libuv is a cross-platform abstraction layer based on event-driven, and Libuv uses asynchronous, event-driven programming. Libuv’s API includes: time (timer), non-blocking network, asynchronous file operation, subprocess, etc. The Event Loop is implemented in Libuv.

Node.js works as follows:

  • The V8 engine parses JS scripts
  • The parsed code calls the Node API
  • The Libuv library is responsible for executing the Node API. It assigns different tasks to different threads, forming an Event Loop that asynchronously returns the execution results of the tasks to the V8 engine.

Six stages

The events in the Libuv engine are divided into six stages, which run repeatedly in sequence. When entering a certain stage, the function will be removed from the corresponding callback queue and executed. When the queue is empty or the number of callback functions executed reaches the threshold set by the system, the next stage will be entered:

As you can roughly see, the events of the event loop are:

External input data --> Polling stage --> Check stage --> Close callback stage --> Timer detection stage --> I/O event callback stage --> Idle (prepare)- > Polling (run repeatedly in this order)...Copy the code
  • timers: Executes the callback that expires in timer(setTimeout, setInterval)
  • pending callbacks: Handles a few unexecuted I/O callbacks from the previous loop
  • idle, prepare: For internal use only
  • poll: The most important stage, executionpending callbackIn the right circumstances will block at this stage
  • checkSetImmediate: Performs the callback to setImmediate(), which inserts the event into the end of the event queue and executes immediately after the main thread and the function of the event queue are completedsetImmediate()The specified return function.
  • close callbacks: Performs the close event callback, for examplesocket.on('close'[, fn])orheep.server.on('close', fn).
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ > │ timers │ < -- -- -setTimeout(),setInterval() callback │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ | | < -- perform all Next Tick Queue and MicroTask Queue callback │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ Pending callbacks <————— Perform I/O callbacks deferred from the previous Tick (to be improved, Negligible) │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ | | < -- perform all Next Tick Queue and MicroTask Queue callback │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ idle, Prepare │ < - - - - - internal call (negligible) │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ | | < -- perform all Next Tick Queue and MicroTask Queue callback | | ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ incoming: │ ├ ─ execute almost all callbacks, │ │ poll │ < ─ ─ ─ ─ ─ ┤ connections, │ besides close callbacks │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ data, Etc. │ and timers scheduling callback │ | | | and setImmediate () scheduling callback, | | └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ at the right time will be blocked at this stage) | | < -- perform all Next Tick Queue and MicroTask Queue callback | ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ check │ < -- -- setImmediate () callback will be at this stage to perform │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ | | < -- perform all Next Tick Queue and MicroTask Queue Callback │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ┤ close callbacks │ < -- -- socket. On (' close ',...). └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘Copy the code

The difference between Node and a browser is that there are several macrotasks in Node, and these different MacroTask queues are ordered differently. Microtask queues are interspersed between each (but not each) MacroTask queue.

It says in the picture:

  • SetTimeout/setInterval belongs to timers

  • SetImmediate is of the check type

  • The close event of a socket is of the type close callbacks

  • All other macro tasks are poll

  • Process. nextTick is a microtask in nature, but it precedes all other microtasks

  • The execution timing of all microtasks is different types of idle/prepare only for internal calls, which we can ignore.

  • Idle /prepare is for internal calls only and can be ignored.

  • Pending callbacks are uncommon and can be ignored.

So based on our experience in browsers, we can conclude:

  • Run all macro tasks with timers task type first, and then all micro tasks. If there is a nextTick, run nextTick first
  • Enter the poll phase and perform almost all macro tasks, followed by all microtasks
  • Execute all macro tasks of type check, and then all microtasks
  • Do all your close’s, do all your microtasks

At this point, I complete an event loop and go back to the Timer phase.

A case in point

setTimeout(() = >{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')})},0)

setTimeout(() = >{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')})},0)
Copy the code

This will print in the browser environment:

timer1
promise1
timer2
promise2
Copy the code

Perform the macro task timer1, then empty all microtasks (promise1), then execute the macro task Timer2, then execute all microtasks (promise2).

But in Node (< 10) the output is different:

timer1
timer2
promise1
promise2
Copy the code

Perform all macro tasks of the same type before performing all microtasks

Of course, there are a few details:

SetTimeout and setImmediate

As mentioned above, The Timer is preceded by the check. However, Node cannot guarantee that the timer will be executed immediately after the preset time, because Node’s expiration detection of timers is not reliable and is affected by system scheduling. For example:

setTimeout(() = > {
  console.log('timeout')},0)

setImmediate(() = > {
  console.log('immediate')})Copy the code

Although the setTimeout delay is 0, Node sets 0 to 1ms, so when the Node prepares eventloop for more than 1ms and enters the timer phase, setTimeout has expired, setTimeout will be executed; otherwise, The Timers phase is then missed and setImmediate is executed first

immediate
timeout
Copy the code

But in one case, the order is fixed:

const fs = require('fs')

fs.readFile('test.txt'.() = > {
  console.log('readFile')
  setTimeout(() = > {
    console.log('timeout')},0)
  setImmediate(() = > {
    console.log('immediate')})})Copy the code

In this case, setTimeout and setImmediate are used to unload THE I/O callcaks, which means that we are in the poll phase and then the Check phase, so setImmediate is implemented first no matter how fast settimerout is.

Poll phase

The poll phase has two main functions:

  • Get new I/O events, perform these I/O callbacks, and then block node here if appropriate
  • When there are immediate or timers that have timed out, perform their callbacks here

The poll phase is used to fetch and perform almost all of the I/O event callbacks, and is an important phase for node Event Loops to continue indefinitely. So its first task is to execute all callbacks in all poll queues synchronously until the queue is empty or the number of callbacks executed reaches a certain limit, and then the poll phase ends.

  1. The setImmediate queue is not empty, then the check phase, then the close callbacks phase…
  2. The setImmediate queue is empty, but timers’ queue is not empty, which leads directly to the Timers stage and then to the poll stage
  3. SetImmediate’s queue is empty and Timers’ queue is also empty, which blocks here because there is nothing left to do.

Pending the callback stage

In libuv’s Event loop, the I/O Callbacks phase executes pending Callbacks. In most cases, all I/O callbacks have already been executed during the POLL phase, but in some cases, some callbacks are deferred until the next cycle. That is, the callback that is executed during the I/O Callbacks phase is the callback that was deferred from the previous event loop.

Strictly speaking, I/O callbacks do not handle file I/O callbacks but rather system call errors such as network Stream, pipe, TCP, udp communication error callbacks.

Refer to the link

  • What is the difference between browser and Node Event loops?
  • Event loop in NodeJS