preface

Event Loop refers to a browser or Node mechanism to solve javaScript single-threaded runtime without blocking, which is often used asynchronous principle.

Why do we need to understand Event Loop

  • It’s about adding depth to your skills, which means understanding how JavaScript works.

  • Now in the front end of a variety of technologies emerge in an endless stream, grasp the underlying principles, you can make the same, should change.

  • Answer the interview of each big Internet company, understand its principle, subject let it play.

Heap, stack, queue

Heap (Heap)

Heap is a data structure, is a complete binary tree maintenance of a set of data, heap is divided into two kinds, one is the maximum heap, one is the minimum heap, the root node of the largest heap is called the maximum heap or large root heap, the root node of the smallest heap is called the minimum heap or small root heap. A heap is a linear data structure, equivalent to a one-dimensional array, with a unique successor.

Such as the maximum heap

The Stack (Stack)

In computer science, a stack is a linear table that is restricted to inserting or deleting only at the end of the table. A stack is a data structure. It stores data on a last-in, first-out (LIFO) basis. The first data to enter is pushed to the bottom of the stack, and the last data is pushed to the top of the stack. A stack is a special linear table that can only be inserted and deleted at one end.

Queue

What makes a queue special is that it allows only deletions at the front of the table and inserts at the rear. Like a stack, a queue is a linear table with limited operations. The end that performs the insert operation is called the tail of the queue, and the end that performs the delete operation is called the head of the queue. When there are no elements in the queue, it is called an empty queue.

The data elements of a queue are also called queue elements. Inserting a queue element into a queue is called enqueuing, and removing a queue element from a queue is called dequeuing. Because queues can only be inserted at one end and deleted at the other end, only the earliest elements in the queue can be removed from the queue first. Therefore, queues are also called FIFO — first in first out (FIFO).

Event Loop

In JavaScript, tasks are divided into two types: macroTasks (also called tasks) and microtasks (microtasks).

MacroTask

  • scriptAll code,setTimeout,setInterval,setImmediate(Browser temporarily not support, only IE10 support, specific visibleMDN),I/O,UI Rendering.

MicroTask

  • Process.nextTick (unique to Node),Promise,Object. Observe (waste),MutationObserver(View the specific usage modehere)

Event Loop in the browser

Javascript has a main thread and a call-stack. All tasks are placed on the call stack and await execution on the main thread.

JS call stack

JS call stack is last in first out rule, when the function is executed, will be added to the top of the stack, when the execution of the stack is completed, it will be removed from the top of the stack, until the stack is cleared.

Synchronous and asynchronous tasks

Javascript is divided into single thread task synchronization task and asynchronous tasks, the synchronization task will wait for the main thread in sequence in the call stack in sequence, after the asynchronous task will result in the asynchronous task, will register the callback function in a task in the queue waiting for the main thread of free time (the call stack is empty), are read to wait for the main thread of execution stack.

Task Queue

Process model for event loops

  • Select the task queue to be executed and select the first task in the task queue. If the task queue is empty, that isnull, the execution jumps to the microtask (MicroTask).
  • Sets the task in the event loop to the selected task.
  • Perform the task.
  • Sets the current running task in the event loop to NULL.
  • Deletes a task that has completed running from the task queue.
  • Microtasks step: Enter the MicroTask checkpoint.
  • Updated interface rendering.
  • Return to step 1.

When execution enters the MicroTask checkpoint, the user agent performs the following steps:

  • Set the MicroTask checkpoint flag to true.
  • When the event loopsmicrotaskWhen execution is not empty: Select the first entrymicrotaskOf the queuemicrotaskTo loop the eventmicrotaskSet to selectedmicrotaskRun,microtaskWhich will have been executedmicrotaskfornull. Remove themicrotaskIn themicrotask.
  • Clean up IndexDB transactions
  • Set the flag for entering the MicroTask checkpoint to false.

The above may not be easy to understand, the following is a picture I made.

After the synchronization Task is executed, the execution stack is checked to see whether the execution stack is empty. If the execution stack is empty, the microTask queue is checked to see whether the microTask queue is empty. If the microTask is empty, the macro Task is executed. After a single macro task is executed, the system checks whether the microTask queue is empty. If it is not empty, the system sets the microTask queue to NULL after all microtasks are executed on a first-in, first-out basis. Then the macro task is executed.

For example

console.log('script start');

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

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

First of all, we divide into several categories:

First execution:

Tasks: run script, setTimeout callback Microtasks:PromiseThen JS stack: script Log: script start, script end.Copy the code

Execute synchronization code that divides macro Tasks and Microtasks into separate queues.

Second execution:

Tasks: run script, setTimeout callback Microtasks: Promise2 then JS stack: Promise2 callback Log: Script start, script end, promise1, promise2Copy the code

After the macro task is executed, if the Microtasks queue is not empty, Promise1 is executed. After the execution of Promise1, Promise2 is called and placed in the Microtasks queue. Then Promise2 is executed.

Third Execution:

Tasks: setTimeout callback Microtasks: JS Stack: setTimeout callback Log: Script start, script end, promisE1, promise2, setTimeoutCopy the code

When the Microtasks queue is empty, the macro task (Tasks) is executed, the setTimeout callback is executed, and the log is printed.

Fourth Execution:

Tasks: setTimeout callback Microtasks: JS stack: Log: script start, script end, promisE1, promisE2, setTimeoutCopy the code

Clear the Tasks queue and JS stack.

Tasks, Microtasks, Queues and Schedules may be easier to understand.

Here’s another 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

We need to understand async/await first.

Async /await is converted to promise and THEN callbacks at the underlying level. That said, this is promise’s grammatical candy. Every time we use await, the interpreter creates a Promise object and puts the remaining async operations in the THEN callback. Async /await implementations are dependent on promises. Literally, async is short for “asynchronous”, while await is short for async wait and can be thought of as waiting for asynchronous method execution to complete.

About the difference between the following version 73 and the 73 version

  • If an earlier version is used, execute the command firstpromise1andpromise2And then to performasync1.
  • In version 73, execute firstasync1To performpromise1andpromise2.

The main reason is because of a specification change in Google (Canary)73, as shown in the figure below:

  • The difference is thatRESOLVE(thenable)The difference between andPromise.resolve(thenable).

In the old version

  • First of all, pass toawaitThe value of is wrapped in aPromiseIn the. The handler is then attached to the wrapperPromiseIn order toPromiseintofulfilledAfter resuming the function, and suspending the execution of the asynchronous function oncepromiseintofulfilledTo resume the execution of the asynchronous function.
  • eachawaitThe engine must create two additional promises (even if there is already one on the rightPromise) and it needs at least threemicrotaskThe queueticks(tickIs the relative time unit of the system, also known as the time base of the system, derived from periodic interrupt (output pulse) of the timer, each interrupt represents onetickAlso known as a “tick of the clock”, a time mark. .

Take an example from Teacher He on Zhihu

async function f() {
  await p
  console.log('ok')}Copy the code

It can be simplified as:


function f() {
  return RESOLVE(p).then((a)= > {
    console.log('ok')})}Copy the code
  • ifRESOLVE(p)forppromiseDirect returnpStudent: Well thenpthethenThe method is called immediately and its callback is immediately enteredjobThe queue.
  • And if theRESOLVE(p)By strict standards, it should be a new onepromise, even though thepromiseSure willresolvep, but the process itself is asynchronous, that is, entering nowjobThe queue is newpromiseresolveProcess, so thepromisethenWill not be called immediately, but will wait until the currentjobThe queue executes to the aboveresolveThe procedure is called, and then its callback (that is, continuesawaitThe following statement is addedjobQueue, so it’s late.

Google (Canary) version 73

  • Use ofPromiseResolveCall to changeawaitThe semantics to be reduced in publicawaitPromiseThe number of conversions in this case.
  • If I pass it toawaitThe value of is already onePromise, then this optimization avoids creating it againPromiseWrappers, in this case, we start from a minimum of threemicrotickTo just onemicrotick.

Detailed process:

73 Versions below

  • First of all, printscript start, the callasync1()Returns onePromiseSo print it outasync2 end.
  • eachawaitA new one will be createdpromise, but the process itself is asynchronous, so theawaitIt will not be called immediately afterwards.
  • Continue to execute the synchronization code, printPromiseandscript endThat will bethenFunction in aMicro tasksQueue for execution.
  • After synchronous execution is complete, checkMicro tasksWhether the queue isnull“And then follow the first in, first out rule.
  • Then print firstpromise1At this time,thenReturns the callback functionundefindeAnd then there isthenChain call, again intoMicro tasksQueue, print againpromise2.
  • Go back toawaitThe location of the execution returnedPromiseresolveDelta function, this is going to turnresolveThrow it into the microtask queue and printasync1 end.
  • whenMicro tasksWhen the queue is empty, execute the macro task and printsetTimeout.

Google (Canary 73)

  • If I pass it toawaitThe value of is already onePromise, then this optimization avoids creating it againPromiseWrappers, in this case, we start from a minimum of threemicrotickTo just onemicrotick.
  • The engine no longer needs to beawaitcreatethrowaway Promise– Most of the time.
  • nowpromisePointing to the same thingPromise“, so there is no need to do this step. Then the engine continues to create as beforethrowaway PromiseTo arrangePromiseReactionJobmicrotaskNext in the queuetickResume the asynchronous function, pause its execution, and return it to the caller.

See (here) for details.

NodeJS the Event Loop

The Event Loop in Node is based on Libuv. Libuv is a new cross-platform abstraction layer of Node. Libuv uses asynchronous, event-driven programming, and its core is to provide I/O Event Loop and asynchronous callback. Libuv’s apis include time, non-blocking networking, asynchronous file operations, child processes, and more. The Event Loop is implemented in libuv.

NodetheEvent loopIt is divided into six stages, and each detail is as follows:

  • timers: performsetTimeoutandsetIntervalIn the maturingcallback.
  • pending callback: a few from the previous cyclecallbackWill be implemented in this phase.
  • idle, prepare: For internal use only.
  • poll: The most important stage, executionpending callbackIn the appropriate case back blocking at this stage.
  • check: performsetImmediate(setImmediate()The event is inserted to the end of the event queue and executed immediately after the main thread and function execution of the event queuesetImmediateThe specified callback functioncallback.
  • close callbacks: performcloseThe eventcallback, e.g.socket.on('close'[,fn])orhttp.server.on('close, fn).

The details are as follows:

timers

In theory, the callback should be executed as soon as the time is up. However, due to the delay of system scheduling, the callback may not be able to reach the expected time. Here’s an example of what the official website documentation explains:

const fs = require('fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout((a)= > {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);


// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation((a)= > {
  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing}});Copy the code

When entering the event loop, it has an empty queue (fs.readfile () has not yet completed), so the timer waits for the remaining milliseconds. When 95ms is reached, fs.readfile () finishes reading the file and its 10-millisecond completion callback is added to the poll queue and executed. When the callback ends, there are no more callbacks in the queue, so the event loop sees that the threshold for the fastest timer has been reached, and then returns to the Timers phase to execute the callback for the timer.

In this example, you will see that the total delay between the timer being scheduled and the callback being executed will be 105 milliseconds.

Here is my test time:

pending callbacks

This phase performs callbacks for certain system operations, such as TCP error types. For example, if TCP socket ECONNREFUSED is trying to receive connect, then some * nix system wants to wait for an error to be reported. This is executed in the Pending Callbacks phase.

poll

The poll phase has two main functions:

  • performI/OThe callback.
  • Processes events in the polling queue.

When the event loops inpollStage and intimersIf no timer can be executed in, one of two things will happen

  • ifpollIf the queue is not empty, the event loop traverses its synchronously executing themcallbackQueue until the queue is empty or reachedsystem-dependent(System related restrictions).

ifpollIf the queue is empty, one of two things can happen

  • If there is a setImmediate() callback to perform, the poll phase is immediately stopped and the check phase is entered to perform the callback.

  • If no setImmediate() goes back to needing to perform, the poll phase waits for the callback to be added to the queue, and then executes immediately.

Of course, if the timer is set and the poll queue is empty, then it will determine if there is a timer timeout, and if there is, it will go back to the timer phase and do a callback.

check

This phase allows the person to perform the callback immediately after the poll phase completes. If the poll phase is idle and the script is queued to setImmediate(), the event loop reaches the check phase for execution instead of continuing to wait.

SetImmediate () is actually a special timer that runs in a separate phase of the event loop. It uses the Libuv API to schedule callbacks to be executed after the poll phase is complete.

Usually, when the code is executed, the event loop eventually reaches the poll phase, which waits for incoming connections, requests, and so on. However, if the callback setImmediate() has been scheduled, and the polling phase becomes idle, it ends and reaches the check phase instead of waiting for a poll event.

console.log('start')
setTimeout((a)= > {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')})},0)
setTimeout((a)= > {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')})},0)
Promise.resolve().then(function() {
  console.log('promise3')})console.log('end')
Copy the code

If the node version is V11.x, the result is the same as that of the browser.

start
end
promise3
timer1
promise1
timer2
promise2

Copy the code

Check out Node’s EventLoop Again, this time node’s Pot.

If there are two cases of the above results in V10:

  • If the time2 timer is already in the execution queue
start
end
promise3
timer1
timer2
promise1
promise2
Copy the code
  • If the time2 timer is not in the execution pair column, the result is
start
end
promise3
timer1
promise1
timer2
promise2
Copy the code

The specific situation can refer to the poll stage of the two cases.

It may be better understood from the following picture:

The difference between setImmediate() and setTimeout()

setImmediateandsetTimeout()Are similar, but behave differently depending on when they are invoked.

  • setImmediate()Designed to be used in the currentpollAfter the check phase is complete, the script is executed.
  • setTimeout()Schedule the script to run after minimum (ms), intimersPhase execution.

For example

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

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

The order in which timers are executed will vary depending on the context in which they are called. If both are called from the main module, the time is limited by process performance.

The results were also inconsistent

If theI / OTo move two calls within a period, the immediate callback is always executed first:

const fs = require('fs');

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

The result must be immediate => timeout. The main reason is that after the file is read in the I/O phase, the event loop enters the poll phase first, and if setImmediate needs to perform, the loop immediately enters the check phase to perform the callback of setImmediate.

Then enter the Timers phase again, run setTimeout, and print timeout.

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ > │ timers │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ pending Callbacks │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ idle, Prepare │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ incoming: │ │ │ poll │ < ─ ─ ─ ─ ─ ┤ connections, │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ data, Etc. │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ check │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ┤ close callbacks │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘Copy the code

Process.nextTick()

process.nextTick()Although it is part of the asynchronous API, it is not shown in the figure. This is becauseprocess.nextTick()Technically, it is not part of the event loop.

  • process.nextTick()Method will becallbackAdded to thenext tickThe queue. Once the tasks of the current event polling queue are complete, innext tickAll in the queuecallbacksWill be called in turn.

Here’s another way to think about it:

  • When each stage is completed, if there isnextTickQueue, all callbacks in the queue are cleared and take precedence over the othersmicrotaskThe execution.

example

let bar;

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

setImmediate((a)= > {
  console.log('setImmediate');
})
function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall((a)= > {
  console.log('bar', bar); / / 1
});

bar = 1;
Copy the code

There are two possible answers to the above code execution in NodeV10. One is:

bar 1
setTimeout
setImmediate
Copy the code

The other is:

bar 1
setImmediate
setTimeout
Copy the code

Either way, process.nexttick (callback) is always executed first, printing bar 1.

The last

Thank you @Dante_hu for asking the question and the article has been corrected. Modified the execution result on the node. Differences between V10 and V11.

The following article is referred to regarding the await issue:.

Normative, Async, Await, Execution Order “Promise, Async, Await, Execution Order” Reduce the number of ticks in async/await “” Async /await execution result is inconsistent in Chrome environment and node environment, solve?” Faster Asynchronous Functions and Promises

Other content reference:

“What is the browser Event Loop (Event Loop)?” Don’t confuse NodeJS with Event Loops in the browser. What’s the difference between Event Loops in the browser and Node? The Node.js Event Loop, Timers, Schedules NextTick () and process.nexttick ()