One, foreword

Event loops are an extremely important foundation for the front end bald player. It is also a very common topic in discussions or interviews. Event Loop is an Event Loop, which is a mechanism for browsers or NodeJS to prevent javaScript from blocking when running in a single thread, which is often used asynchronously. The main function of this article is to get through the cycle of eventsCopy the code

Second, the overview

Understanding JavaScript event loops often comes with issues related to execution stacks, task queues (macro tasks, microtasks), JavaScript single-threaded execution, and browser asyncrony. The event loop implementation in browsers and Nodes is also quite different. Getting familiar with event loops and understanding how browsers run will help us understand how JavaScript executes and troubleshoot runtime problems.Copy the code

Stack and execution stack

A Stack, "Stack," in computer science, is a linear table that limits insertion or deletion to the end of a table. A stack is a data structure that stores data according to the principle of last in, first out. The data entered first is pushed to the bottom of the stack, and the last data is placed at the top of the stack. When data needs to be read, the data will be ejected from the top of the stack. The nature of the stack is first in, last out.Copy the code

Javascript has a main thread and a call-stack. All tasks are put on the call stack to wait for the main thread to execute.

Queues and task queues

A Queue is also a linear list with limited operations. In particular, it only allows delete operations on the front end of the table, whereas insert operations on the rear end of the tableCopy the code

When parsing a piece of code, JS will hand over the asynchronous task to other threads for processing, and when the asynchronous task is completed, it will put the callback into the task queue to be executed stack.

Tasks are divided into macro tasks and micro tasks (common)

The principle of asynchronous JavaScript execution in browsers

JS is single-threaded, meaning that only one task can be executed at a time, so why can browsers execute asynchronous tasks at the same time? Because browsers are multithreaded, when the JS needs to perform an asynchronous task in the browser, the browser starts another thread. JS is single-threaded because browsers provide only one JS engine thread (the main thread) to run THE JS code. But there are threads in the browser such as timer threads and HTTP request threads, and these threads are not primarily used to run JS code. For example, if the main thread needs to send an AXIos request, it will hand it off to another browser thread (the HTTP request thread) to actually send the request, and when the request comes back, The browser is responsible for sending the request and JS is only responsible for performing the final callback processing. So the asynchrony here is not the IMPLEMENTATION of JS itself, in fact, is the ability provided by the browser.Copy the code

Browser not only with multiple threads, as well as multiple processes, such as, the GPU rendering process and plug-in process as a front-end developer, mainly focus on the rendering process, the rendering process includes the JS engine thread, HTTP request thread and timer thread, etc., these threads for JS asynchronous task provides the basis in the browser.

4. Event loops in browsers

When the JS engine thread runs the JS code, it will queue the synchronous code in the execution stack in order, and then execute the functions inside in turn. When an asynchronous task is encountered, it will be handed over to other threads for processing. After all synchronous codes in the current execution stack are executed, the callback of the completed asynchronous task will be taken out from the task queue and added to the execution stack to continue execution. When an asynchronous task is encountered, it will be handed over to other threads again. . And so on. When other asynchronous tasks complete, callbacks are placed on the task queue to be executed stack.

JS executes the methods in the stack sequentially. Each time a method is executed, a unique execution context will be generated for the method. After the method is executed, the current execution context will be destroyed, and the method will be popped from the stack (that is, consumable), and then proceed to the next method.

As you can see, in event-driven mode, at least one execution loop is included to detect new tasks in the task queue. This process is called an event loop, and each loop is an event cycle or tick.

During the event cycle, the execution stack first checks whether there is any task to be executed in the microtask queue after the synchronous code execution is completed. If not, it checks whether there is any task to be executed in the macro task queue again and again. Microtasks are usually executed first in the current loop, while macro tasks wait until the next loop. Therefore, microtasks are usually executed before macro tasks, and there is only one microtask queue, while macro task queues may be multiple. Other common events such as clicks and keyboards are also macro tasks.

You can also think of microtasks as being performed at the end of the current event loop; Macro tasks are performed at the beginning of the next event cycle.

Essential differences between microtasks and macro tasks

When a JS encounters an asynchronous task, it offloads the task to another thread, and its main thread continues to perform the synchronous task later. Such assetTimeoutIs handled by the browser's timer thread. When the timer ends, the timer callback task is placed in the task queue for the main thread to retrieve and execute. Because JS is single-threaded, asynchronous tasks need to be performed by other browser threads. When promise.then is executed, the V8 engine does not delegate the asynchronous task to other threads in the browser. Instead, it stores the callback in a queue of its own and executes the promise.then queue as soon as the current stack completes execution. Even in some ways, microtasks aren't entirely asynchronous; they just change the order in which the code is written.setTimeoutThere is a "timed wait" task that requires the timer thread to execute; Ajax requests have "send request" tasks that need to be performed by HTTP threads, promise. Then it doesn't have any asynchronous tasks that need to be performed by other threads, it only has callbacks, and if it does, it's just another macro task nested internally.Copy the code
  • Macro task characteristics:  There are explicit asynchronous tasks to execute and call back; Additional asynchronous thread support is required.
  • Features of microtasks:  There are no explicit asynchronous tasks to execute, only callbacks; No additional asynchronous thread support is required.

Event loop quiz 1

console.log('start')
async function async1() {
    console.log('a')
    await async2()
    console.log('b')}async function async2() {
    console.log('c')
}
async1()

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

new Promise(resolve= > {
        console.log('e')
        resolve()
    })
    .then(function() {
        console.log('f')
    })
    .then(function() {
        console.log('g')})console.log('end')
Start ->a-> C -> E ->end->b-> F ->g->d
Copy the code
Sequential analysis of execution
  1. Execute code, outputstart
  2. Execute async1() and print firstaAsync2 () is called and printedc, the async1 function will remain in the context, and then the first microtask will be generated out of the async1 function (” await “if it is a variable or no callback function)So let's keep this in mind
  3. When setTimeOut is encountered, a macro task is generated
  4. Execute the Promise, outputeWhen then is encountered, the second microtask is generated
  5. Continue to execute code outputend
  6. After the code logic completes (the current macro task completes), start executing the microtask generated by the macro task, and execute the remaining code output of async1()b
  7. Execute the next microtask output in sequencefCreate a new microtask and execute the outputg
  8. Why print with undefinedPerforming an await results in a promise return
let promise_ = new Promise((resolve,reject){ resolve(undefined)})
Copy the code
  1. Finally, execute the next macro task, setTimeout, outputd

Event loop quiz 2(Note when the callback function after await is registered as a microtask)

console.log('start')
async function async1() {
    console.log('a')
    await async2()
    console.log('b')}async function async2() {
    return Promise.resolve().then(() = >{ 
         console.log('c')
    })
}
async1()

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

new Promise(resolve= > {
        console.log('e')
        resolve()
    })
    .then(function() {
        console.log('f')
    })
    .then(function() {
        console.log('g')})console.log('end')
Start -> A -> E ->end->c-> F ->g->b->d
Copy the code

Note that: Awit does not register the code following “await” in the microtask queue. Instead, after executing “await”, awIT immediately steps out of async1 and executes other code. After executing the other code, it needs to go back to async1 to execute the rest of the code. Then register the code following the await to the microtask queue

Timer error

To understand the error of the timer is zhang Fei eating bean sprouts - a piece of cake in the event loop, synchronous code is always executed first, then asynchronous callback to the task queue to execute. When you performsetTimeoutBrowser, start a new timer thread to timing, timing trigger after the timer event callback to deposited in the macro task queue waiting for JS the main thread to remove the execution, if it's the main thread and still in the process of synchronization tasks, then macro task at this time only to hang up first, and timer inaccurate problem, synchronization code takes longer, The more wrong the timer is. Not only do synchronized code, microtasks can also affect timing because microtasks take precedence. If there is an infinite loop in synchronized code or recursion in a microtask constantly starts other microtasks, the code in a macro task may never execute.Copy the code

View Render update

Micro task queue execution is completed, that is, one end of the event loop, the browser will execute view rendering, of course, there will be the browser's optimization, may be the result of the merge multiple cycle for a view re-paint, so view is updated after the event loop, so not every operation Dom will instantly refresh the view. The requestAnimationFrame callback is executed before the view is redrawnCopy the code

NodeJS event loops

The JS engine itself is not a first event loop mechanism, this is implemented by its host (browser), and NodeJS has its own event loop implementation. NodeJS is also a loop + task queue process, and microtasks take precedence over macro tasks, which is roughly the same as the browser, but there are some differences, adding some task types and task stages.Copy the code

Asynchronous methods in NodeJS

Since both are based on the V8 engine, the asynchrony contained in the browser is the same as in NodeJS. - File I/O: asynchronously loads local files. - setImmediate() : Does not mediatesetTimeout-process.nexttick () : -server.close, socket.on() -server.close, socket.on()'close',...). Etc. : Close the callbackCopy the code

Event cycle model

NodeJS ‘cross-platform capabilities and event loops are based on the Libuv library. In NodeJS, the V8 engine parses the JS code and invokes the Node API. Then the Node API assigns tasks to Libuv, and finally returns the execution results to the V8 engine. An event loop process is implemented in Libux to manage the execution of these tasks, so NodeJS event loops are mostly done in Libuv

Stages of the event cycle

In NodeJS execution, we mainly need to pay attention to the following stages. Each of the following stages has its own task queue. When the execution reaches the corresponding stage, we judge whether there are tasks to be processed in the task queue at the current stage.Copy the code
  • Timers phase
To perform allsetTimeout() andsetInterval() callbackCopy the code
  • Pending callbacks phase
A callback for some system operation, such as a TCP connection error. Most callbacks except timers, Close, and setImmediate are performed at this stage (a few callbacks from the previous loop are performed at this stage).Copy the code
  • Poll phase
Polling waits for events like new links and requests, performs I/O callbacks, and so on. The V8 engine enters this phase first after parsing the JS code and passing it to the Libuv engine. If the task queue is exhausted at this stage, enter the Check phase to perform the setImmediate callback (if setImmediate exists), or wait for a new task to come in (if no setImmediate exists). If an TIMERS timer expires while waiting for a new task, the timers phase is directly entered. This phase may block the wait.Copy the code
  • The check phase
SetImmediate () is a callback that inserts an event into the end of the event queue and executes the setImmediate specified callback immediately after the main thread and the event queue function have completedCopy the code
  • The close callbacks phase
Turn off callback execution, such as socket.on('close',...).Copy the code
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ > │ timers │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ pending Callbacks │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ idle, Prepare (internal use) │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ incoming: │ │ │ poll │ < ─ ─ ─ ─ ─ ┤ connections, │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ data, Etc. │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ check │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ┤ close callbacks │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘Copy the code

Each of the above stages will complete the task queue of the current stage, and then continue to execute the microtask queue of the current stage. Only when all the microtasks of the current stage are completed, the next stage will be entered. This is also where the logic differs significantly from that of the browser, but the browser does not distinguish between these phases and has fewer types of asynchronous operations, so there is no need to distinguish between the two

NodeJS event loop output test

const fs = require('fs');
fs.readFile(__filename, (data) = > {
  	// Poll (I/O callback) phase
    console.log('readFile')
    Promise.resolve().then(() = > {
        console.error('promise1')})Promise.resolve().then(() = > {
        console.error('promise2')})});setTimeout(() = > {
  	/ / timers
    console.log('timeout');
    Promise.resolve().then(() = > {
        console.error('promise3')})Promise.resolve().then(() = > {
        console.error('promise4')})},0);
// The following code just blocks synchronously for 1 second to ensure that the above asynchronous task is ready
var startTime = new Date().getTime();
var endTime = startTime;
while(endTime - startTime < 1000) {
    endTime = new Date().getTime();
}
Timeout promise3 promise4 readFile promise1 promise2
Copy the code

The difference with the browser is also reflected in the different tasks performed in the same phase (existed before NodeJS 10 and is now consistent with the browser).

NextTick, setImmediate and setTimeout

We often use it in actual projectsPromiseorsetTimeoutTo perform tasks that require delay, such as time-consuming calculations or log uploads, and do not want their execution to take up time on the main thread or depend on the results of the entire synchronized code execution. Process.nexttick () and setImmediate() in NodeJS have similar effects. SetImmediate () is performed during the check phase, while process.nexttick () is not the same. It does so earlier than promise.then(), after the synchronous task and before all other asynchronous tasks, Prioritizing nextTick can be imagined as putting the nextTick task later in the current loop, similar to promise.then() but ahead of promise.then().Copy the code
let bar;
setTimeout(() = > {
  console.log('setTimeout');
}, 0)
setImmediate(() = > {
  console.log('setImmediate');
})
function someAsyncApiCall(callback) {
  process.nextTick(callback);
}
someAsyncApiCall(() = > {
  console.log('bar', bar); 
});
bar = 1;
// Prints 1
Copy the code

The difference between setImmediate and setTimeout

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

  • setImmediate()Designed to be used in the presentpollAfter the phase is complete, the script is executed in the check phase.
  • setTimeout()Schedule scripts to run after minimal (ms), intimersPhase execution.
For example
setTimeout(() = > {
  console.log('timeout');
}, 0);
setImmediate(() = > {
  console.log('immediate');
});
// Output: timeout, setImmediate
Copy the code

After the first loop, setTimeout and setImmediate are added to the task queue for their respective stages, respectively. The second loop first enters the Timers phase, where timer queue callbacks are performed, then the Pending Callbacks and Poll phases have no tasks, so it enters the Check phase to perform the setImmediate callback. So the final output is timeout, setImmediate

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

If theI / OMoving two calls in a cycle always executes the immediate callback first:

const fs = require('fs');

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

The result is immediate => timeout. The main reason is that after the FILE is read in the I/O phase, the event loop goes to the Poll phase first, and the setImmediate callback is immediately executed in the Check phase. Then enter the timers stage and run setTimeout to print timeout.

Finally, the content of the article is referred to

Understanding Event Loop once (Once and for all)

Eventloop)