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
script
All 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 is
null
, 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 loops
microtask
When execution is not empty: Select the first entrymicrotask
Of the queuemicrotask
To loop the eventmicrotask
Set to selectedmicrotask
Run,microtask
Which will have been executedmicrotask
fornull
. Remove themicrotask
In 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 first
promise1
andpromise2
And then to performasync1
. - In version 73, execute first
async1
To performpromise1
andpromise2
.
The main reason is because of a specification change in Google (Canary)73, as shown in the figure below:
- The difference is that
RESOLVE(thenable)
The difference between andPromise.resolve(thenable)
.
In the old version
- First of all, pass to
await
The value of is wrapped in aPromise
In the. The handler is then attached to the wrapperPromise
In order toPromise
intofulfilled
After resuming the function, and suspending the execution of the asynchronous function oncepromise
intofulfilled
To resume the execution of the asynchronous function. - each
await
The engine must create two additional promises (even if there is already one on the rightPromise
) and it needs at least threemicrotask
The queueticks
(tick
Is 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 onetick
Also 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
- if
RESOLVE(p)
forp
为promise
Direct returnp
Student: Well thenp
thethen
The method is called immediately and its callback is immediately enteredjob
The queue. - And if the
RESOLVE(p)
By strict standards, it should be a new onepromise
, even though thepromise
Sure willresolve
为p
, but the process itself is asynchronous, that is, entering nowjob
The queue is newpromise
的resolve
Process, so thepromise
的then
Will not be called immediately, but will wait until the currentjob
The queue executes to the aboveresolve
The procedure is called, and then its callback (that is, continuesawait
The following statement is addedjob
Queue, so it’s late.
Google (Canary) version 73
- Use of
PromiseResolve
Call to changeawait
The semantics to be reduced in publicawaitPromise
The number of conversions in this case. - If I pass it to
await
The value of is already onePromise
, then this optimization avoids creating it againPromise
Wrappers, in this case, we start from a minimum of threemicrotick
To just onemicrotick
.
Detailed process:
73 Versions below
- First of all, print
script start
, the callasync1()
Returns onePromise
So print it outasync2 end
. - each
await
A new one will be createdpromise
, but the process itself is asynchronous, so theawait
It will not be called immediately afterwards. - Continue to execute the synchronization code, print
Promise
andscript end
That will bethen
Function in aMicro tasksQueue for execution. - After synchronous execution is complete, checkMicro tasksWhether the queue is
null
“And then follow the first in, first out rule. - Then print first
promise1
At this time,then
Returns the callback functionundefinde
And then there isthen
Chain call, again intoMicro tasksQueue, print againpromise2
. - Go back to
await
The location of the execution returnedPromise
的resolve
Delta function, this is going to turnresolve
Throw it into the microtask queue and printasync1 end
. - whenMicro tasksWhen the queue is empty, execute the macro task and print
setTimeout
.
Google (Canary 73)
- If I pass it to
await
The value of is already onePromise
, then this optimization avoids creating it againPromise
Wrappers, in this case, we start from a minimum of threemicrotick
To just onemicrotick
. - The engine no longer needs to be
await
createthrowaway Promise
– Most of the time. - now
promise
Pointing to the same thingPromise
“, so there is no need to do this step. Then the engine continues to create as beforethrowaway Promise
To arrangePromiseReactionJob
在microtask
Next in the queuetick
Resume 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.
Node
theEvent loop
It is divided into six stages, and each detail is as follows:
timers
: performsetTimeout
andsetInterval
In the maturingcallback
.pending callback
: a few from the previous cyclecallback
Will be implemented in this phase.idle, prepare
: For internal use only.poll
: The most important stage, executionpending callback
In 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 queuesetImmediate
The specified callback functioncallback
.close callbacks
: performclose
The 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:
- perform
I/O
The callback. - Processes events in the polling queue.
When the event loops inpoll
Stage and intimers
If no timer can be executed in, one of two things will happen
- if
poll
If the queue is not empty, the event loop traverses its synchronously executing themcallback
Queue until the queue is empty or reachedsystem-dependent
(System related restrictions).
ifpoll
If 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()
setImmediate
andsetTimeout()
Are similar, but behave differently depending on when they are invoked.
setImmediate()
Designed to be used in the currentpoll
After the check phase is complete, the script is executed.setTimeout()
Schedule the script to run after minimum (ms), intimers
Phase 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 / O
To 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 becallback
Added to thenext tick
The queue. Once the tasks of the current event polling queue are complete, innext tick
All in the queuecallbacks
Will be called in turn.
Here’s another way to think about it:
- When each stage is completed, if there is
nextTick
Queue, all callbacks in the queue are cleared and take precedence over the othersmicrotask
The 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 ()