Introduction (Background)
There are many articles on the Internet explaining Event Loop, but most of them are too one-sided. A few relatively comprehensive articles still have some knowledge blind spots, and some articles even spread wrong knowledge, which makes us vaguely understand. This article will make you understand everything!
Get started
If you already know about JS data types, stacks, heaps, queues, execution stacks, and execution contexts, you can skip this section.
JS data type
JS data types fall into two broad categories: primitive and reference types
There are seven primitive types:
number
string
boolean
undefined
symbol (ES2015)
bigint (ES2020)
Null (special primitive type)
typeof Awesome!= = ='number' // true
typeof 'str'= = ='string' // true
typeof false= = ='boolean' // true
typeof undefined= = ='undefined' // true
typeof Symbol('sym') = = ='symbol' // true
typeof 666n= = ='bigint' // true
typeof null= = ='object' // true
// Different objects are represented as binary at the bottom,
// In JavaScript, zeros in the first three bits of binary are considered object,
// The binary representation of null is all zeros, so the first three digits are also zeros, so typeof returns "object"
Copy the code
There is only one Object reference type:
Object
const fn = (x) = > x
fn instanceof Function // true
fn instanceof Object // true
Copy the code
So Function is a subtype of Object, which involves the knowledge of prototype chain
In fact, Object also contains many subtypes, such as Array, RegExp, Math, Map, Set, WeakMap, WeakSet, etc
Special note: ECMAScript provides three special reference types: Number, String, and Boolean to facilitate manipulation of primitive types. These types have the same characteristics as the other reference types described above, but also have special behaviors corresponding to their respective primitive types. Every time a method or property of a primitive type is used, an object of the corresponding primitive wrapper type is created in the background, exposing the various methods that operate on the primitive type. Consider the following example:
let s1 = 'some text'
let s2 = s1.substring(2)
// The above is equivalent to the following operation
let s1 = new String('some text')
let s2 = s1.substring(2)
s1 = null
/ / for example 1
let s1 = 'some text'
s1.color = 'red'
console.log(s1.color) // undefined
// The reason is that the second line creates a temporary String object, and when the third line executes, the object has already been destroyed
// Actually, the third line of code creates its own String object here, but this object has no color attribute.
/ / 2
let obj = new Object('some text')
console.log(obj instanceof String) // true
// If the Object is passed a String, an instance of String is created.
/ / for example 3
let value = '25'
let number = Number(value) // Transition function
console.log(typeof number) // "number"
let obj = new Number(value) // constructor
console.log(typeof obj) // "object"
// If it is a numeric value, an instance of Number is created. Boolean values give you an instance of Boolean.
/ / for example 4
let falseObject = new Boolean(false)
let result = falseObject && true
console.log(result) // true
let falseValue = false
result = falseValue && true
console.log(result) // false
console.log(typeof falseObject) // object
console.log(typeof falseValue) // boolean
console.log(falseObject instanceof Boolean) // true
console.log(falseValue instanceof Boolean) // false
Copy the code
The three special reference types above: Number, String, and Boolean may be mentally taxing for you, I’m sorry ~ but it’s an interesting fact
The stack (stack)
Stack, heap and queue are the three data structures of JS
The characteristics of the stack are: the exit and the entrance are the same, followAfter the advanced
,Last in, first out
The principle of. Data can only be pushed sequentially and removed sequentially.
A picture is worth a thousand words:
Heap (heap)
The characteristics of the heap areA disorderly
的 key-value
Key/value pair
Storage mode.
The access mode of the heap has no relation to the order, not limited to the entrance and exit.
A picture is worth a thousand words:
We want to find the book on the shelf, the most direct way is to look up the title, the title is our key. With this key, you can easily get the corresponding book.
When executing javascript code, different variables are stored in different places in memory. Stack memory: holds the raw data type and the pointer heap that references the data type. However, when there are closures, the variables used by 'closures' (raw data types, reference data types) are stored in the [[Scopes]] (function properties that are stored in the function's Closure chain, where the Closure represents the Closure) and then stored in the heap. To save mental strain, this article does not cover closures.Copy the code
Those of you who are interested can click on MDN to learn about closures
The following figure helps you understand stack memory and heap memory:
Why is there stack memory and heap memory?
Usually related to garbage collection mechanisms. To minimize the memory footprint of the program when it runs.
When a method to perform, every method can set up their own memory stack variables defined in this way will be one by one into the stack memory, with the method of implement the end, the method of memory stack will destroy the nature, the inside of the memory stack data is cleared (closure is an exception, memory stack destroyed, but the inside of the closure if use the memory stack data, Closures store data. Closures can’t be messed with. Therefore, all variables defined in a method are placed in stack memory;
When we create an object in a program, the object is stored in the runtime data area for reuse (because objects are usually expensive to create), and this runtime data area is heap memory. Will not end with the method of objects in the heap memory and destruction, even after the method, the object may also be another reference variables referenced, this object is not destroyed, only when there is no reference to an object variable reference it, garbage collection system will be in a certain time to recycle it. (See the garbage collection mechanism of JS runtime environment)
Queue
The characteristics of queues areFirst in first out
Data access is “inserted from the back of the queue, taken from the head of the queue”.
The difference with stack: the stack is stored and removed at the top of an exit, while the queue is divided into two, one exit, one entrance
A picture is worth a thousand words:
Execute JS stack
The execution stack is also called the execution context stack or the call stack
When a program executes into an execution environment (such as an entire script tag or inside a function), its execution context is created, which contains the parameters and local variables of the function and is pushed onto the execution stack (pushed). When the program completes execution, its execution context is destroyed and pushed off the top of the stack to proceed to the next execution context.
Because the global environment is entered first in JS execution, the execution context of the global environment starts at the bottom of the stack. At the top of the stack is the execution context of the currently executing function, which is pushed off the top of the stack when the function call completes.
Note that the stack memory mentioned above is actually contained in the execution stack, or that the stack memory is the execution stack, but the concept of execution context is not mentioned above.
function foo() {
function bar() {
var str = 'string'
console.log('I am bar')
}
bar()
}
foo()
Copy the code
A picture is worth a thousand words:
Execution context
Concept: The following is excerpted"JavaScript Advanced Programming -- Edition 4"
Chapter 4, verse 2
The concept of an execution context (” context “) is important in JavaScript. The context of variables or functions determines what data they can access and how they behave. Each context has an associated variable object on which all variables and functions defined in that context reside. Although the variable object cannot be accessed through code, it is used for data processing in the background.
The global context is the outermost context. Depending on the hosting environment of the ECMAScript implementation, the object representing the global context may be different. In browsers, the global context is what we call the Window object (more on that in Chapter 12), so all global variables and functions defined by var become properties and methods of the Window object. Top-level declarations using lets and const are not defined in the global context, but have the same effect on scope-chain resolution. The context is destroyed after all of its code has been executed, including all variables and functions defined on it (the global context is destroyed before the application exits, such as closing the web page or exiting the browser).
Each function call has its own context. When the code execution stream enters a function, the context of the function is pushed onto an execution stack. After the function completes execution, the execution stack pops up the function context, returning control to the previous execution context. The execution flow of an ECMAScript program is controlled through this execution stack.
When the code in the context executes, it creates a scope chain of variable objects. This chain of scopes determines the order in which the code at each level of context accesses variables and functions. The variable object of the context in which the code is executing is always at the front of the scope chain. If the context is a function, its activation object is used as a variable object. Active objects initially have only one definition variable: arguments. (This variable is not available in the global context.) The next variable object in the scope chain comes from the containing context, and the next object comes from the containing context. And so on up to the global context; The variable object of the global context is always the last variable object in the scope chain.
Identifier resolution at code execution is done by searching identifier names down the scope chain. The search process always starts at the very top of the scope chain and moves down until the identifier is found. (If the identifier is not found, an error is usually reported.)
In plain English: An execution context is a piece of code. This is a metaphor. Don’t ask me why
// In the stack example above
// The global execution context is this
function foo() {
function bar() {
var str = 'string'
console.log('I am bar')
}
bar()
}
foo()
// Foo () creates this execution context
function bar() {
var str = 'string'
console.log('I am bar')
}
bar()
// The execution context created by bar() is this
var str = 'string'
console.log('I am bar')
// Each context has a chain of scopes for searching variables and functions, and some, such as this, require an abstract understanding ~.~
Copy the code
Summary of pre-knowledge points
The following is excerpted"JavaScript Advanced Programming -- Edition 4"
Chapter 4, verse 4
- The original value is fixed in size and therefore stored in stack memory
- Copying the original value from one variable to another creates a second copy of the value
- Reference values are objects stored in heap memory
- Variables that contain reference values actually contain only a pointer to the corresponding object, not the object itself
- Copying a reference value from one variable to another only copies Pointers, so the result is that both variables point to the same object
typeof
The operator can determine the original type of a value, whileinstanceof
The operator is used to ensure the reference type of a value- Any variable (whether it contains a raw value or a reference value) exists in some execution context (also known as scope). This context (scope) determines the lifetime of variables and what parts of the code they can access
- Execution context segmentation
Global context
,Function context
andBlock-level context
- Each time the code execution flow enters a new context, one is created
The scope chain
, used to search variables and functions - Of a function or block
Local context
Not only can you access variables in your own scope, but you can also access any variableInclude context
And evenGlobal context
The variables in the - The global context is accessible only
Global context
Cannot directly access any data in the local context - The execution context of a variable is used to determine when
Free memory
JavaScript
Is a programming language that uses garbage collection, so developers don’t have to worry about memory allocation and collection.JavaScript
The garbage collection program can be summarized below- Values that leave scope are automatically marked as recyclable and then deleted during garbage collection
- The dominant garbage collection algorithm is tag cleaning, which marks values that are not currently in use and then comes back to reclaim their memory
- Reference counting is another garbage collection strategy that keeps track of how many times a value is referenced.
JavaScript
The engine no longer uses this algorithm, but some older versions of IE are still affected by this algorithm becauseJavaScript
Will access non-nativeJavaScript
Objects, such as DOM elements - Reference counting has problems with circular references in your code
- Dereferencing variables not only eliminates circular references, but also helps with garbage collection. To facilitate memory reclamation, global objects, properties of global objects, and circular references should all be dereferenced when no longer needed
Focus (Event Loop)
One of the hallmarks of JavaScript is a single thread that has a unique event loop.
::: tip why single thread?
If JavaScript is multithreaded, what happens when two threads simultaneously perform an operation on the DOM, such as one adding an event to it and the other deleting the DOM? You might argue, can’t we use Web workers to create multithreaded environments? This is because there are many restrictions on using Web workers. DOM nodes cannot be operated directly in workers, nor objects such as window and parent can be used. However, the Worker thread can get the Navigator object and the Location object. You can click the MDN link for details. In summary, we can think of JavaScript as single-threaded.
: : :
The following is excerptedJavaScript You Didn't Know -- Middle Volume
Chapter 1, verse 7
In fact, JavaScript programs are always divided into at least two blocks: the first one runs now; The next block will run in response to an event. Although the program is executed piece by piece, all of these blocks share access to program scope and state, so changes to state are made on top of previous cumulative changes
As soon as there are events to run, the event loop runs until the queue empties. Each round of the event cycle is called a tick. User interactions, IO, and timers add events to the event queue.
Only one event can be processed from the queue at any time. When an event is executed, one or more subsequent events may be directly or indirectly raised.
Concurrency is when two or more chains of events are executed alternately over time, so that at a higher level, they appear to be running at the same time (although only one event is being processed at any one time).
These concurrently executing “processes” (as distinct from the concept of a process in an operating system) usually require some form of interactive coordination, such as the need to ensure execution order or the need to prevent races. These “processes” can also be divided into smaller chunks for other “processes” to insert.
The basic concept
::: tip Describes basic concepts
JavaScript
Code execution process, in addition to rely onExecution stack
In addition to determining the order in which functions are executed, it also depends on the task queuetask queue
To getAsynchronous code
The execution.- Event loops are unique within a thread, but task queues can have multiple
- The task queue is subdivided into
macro-task
(macro task) andmicro-task
(Microtask), inECMAScript
,macro-task
Referred to astask
,micro-task
Referred to asjobs
macro-task
These include:Script (whole code)
.setTimeout
.setInterval
.setImmediate
.I/O
.UI rendering
micro-task
These include:process.nextTick
.Promise
.MutationObserver
setTimeout/Promise
And so we call it the task source. What enters the task queue is the specific execution task they specify- Tasks from different task sources enter different task queues. Among them
setTimeout
与setInterval
Is the homologous - Each of these tasks is performed, both
macro-task
ormicro-task
, with the help ofExecution stack
To complete the
: : :
setTimeout(function () {
console.log(Awesome!)},1000)
// Some students have a wrong understanding of setTimeout. Here's a rundown:
The callback function in setTimeout is the task that enters the task queue
// setTimeout as a task dispenser, this function executes immediately,
// The task it assigns, its first argument, is deferred execution
Copy the code
// Similar async functions
const { log } = console
const one = () = > {
log(1)
return Promise.resolve(2)}async function myFunc() {
log(4)
const res = await one()
log(res)
log(5)
}
log(6)
myFunc()
log(7)
// 5
// async is synchronous code until it hits await keyword,
// The whole function is suspended only when an asynchronous task is encountered.
// The rest of the async function is resumed after the async code is cleared from the stack
// It will be easier to come back
Copy the code
::: Warning Sequence of event loops The sequence of events that determines the order in which JavaScript code is executed.
- It is from
Script (whole code)
Start the first loop. The global context then enters the execution stack. untilExecution stack
empty - And then execute all of them
micro-task
. When all executablemicro-task
After execution, the loop ends. - The next cycle starts again from
macro-task
Start, findmacro-task
The first task in theExecution stack
. - Then do it again
macro-task
After the completion of the synchronization code, execute all of the macro tasksmicro-task
. - Another cycle ends. And so on and so on.
When a setTimeout is encountered during the execution of a setTimeout task, it will still dispatch the corresponding task to the Macro-Task queue, but the task will have to wait for the next event loop to execute.
In order to reduce the mental burden, I left out some content, so in the browser environment:
-
When is requestAnimationFrame called
According to the MDN documentation, the browser calls the specified callback function to update the animation before the next UI render. This method requires passing in as an argument a callback function that is executed before the browser’s next rendering
-
When to render the UI
According to the HTML Standard, UI render begins after one round of event loop execution and before the next. If UI render is executed, call requestAnimationFrame and then execute UI Render. The UI render is done and then the next loop;
-
When is requestIdleCallback called
RequestIdleCallback executes this every time it checks to see if the render is finished and finds that there is still time to refresh the next frame. If I don’t have enough time, I’ll talk about it in the next frame.
-> requestAnimationFrame -> requestIdleCallback -> UI render :::
Those interested in requestAnimationFrame can click on the MDN link
Those interested in requestIdleCallback can click on the MDN link
A picture is worth a thousand words:
A simple question
Let’s do a simple problem:
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')
// Output result:
// script start
// script end
// promise1
// promise2
// setTimeout
// First event loop: script start script end promise1 promise2
// The second event loop: setTimeout
Copy the code
Did you do it right? Did the right thing? That’s great! Haven’t done it right? Let me explain it to you
- The first loop starts with script(the whole code). Then the global context enters the execution stack,
- Execute the code, printing script start
- Then meet
setTimeout
, know it is a macro task in asynchronous code, turnfunction () { console.log('setTimeout') }
Put into the macro task queue - Then meet
Promise.resolve().then
, knowing that it is a microtask in asynchronous codefunction () { console.log('promise1') }
Put into the microtask queue - Execute code, print script end
- At this time
Script (whole code)
After the command is executed, a dialog box is displayedExecution stack
Then check if there are anyMicro tasks
. - When a microtask is found in the microtask queue, the first microtask in the microtask queue is removed from the microtask queue and put into the microtask queue
Execution stack
- Execute code, printpromise1, implicit
return undefined
Encountered,.then
Add one to the microtask queuefunction () { console.log('promise1') }
And then out of the stack; - Keep checking to see if
Micro tasks
If yes, add itExecution stack
. - Execute code, printpromise2Continue to check if there is
Micro tasks
, find no, end of this cycle. Operations before the next cycle begins
: Check whether toTo render
Page to determine whether to executerequestAnimationFrame
requestIdleCallback
UI render
; After the- Start the next cycle, check
Macro task queue
In, whether there is a task, found yesfunction () { console.log('setTimeout') }
And in theExecution stack
- Execute the code and print setTimeout
- Check the microtask queue, find no, the loop ends, execute
Operations before the next cycle begins
Here’s a recommended site to help you understand the event cycle by clicking on links
One more question:
var i = 0
setTimeout(() = > {
console.log('setTimeout')})function recursionMicrotask() {
console.log(Awesome!)
if (i < 500) {
i++
Promise.resolve().then(() = > {
recursionMicrotask()
})
} else {
console.log('end')
}
}
recursionMicrotask()
// Comment out the if else if you want to experience page freezes
Copy the code
This problem is to let you intuitive feel, micro-tasks can be infinite to create micro-tasks, resulting in the next round of the cycle, if the micro-task will not end will cause the page stuck ~
Summary (Event loop so esay)
Browser environment:
Macro tasks: setTimeout, setInterval, Script (overall code), UI event interaction, postMessage, MessageChannel, XMLHttpRequest microtasks: Promise.then/catch, MutationObserver, queueMicrotask (chrome71+ only supports), requestAnimationFrame (for good memory, it is not, It is the callback function before the UI render execution of the microtask ends.Copy the code
Node environment: V11+
Macro tasks: setTimeout, setInterval, setImmediate, I/O Then /catch, process.nextTick (priority over other microtasks), and queueMicrotask (supported only by V11.0 +) Node event loops are divided into six stages: timers: Timer phase, used to handle setTimeout and setInterval's callback function pending callbacks. Callbacks used to perform certain system operations, such as TCP error idle, prepare: Poll: The poll phase, which executes the I/O queue, and checks the timer to see if the time is up check: setImmediate close callbacks To handle closed callbacks, such as socket.destroy(), each phase has its own macro queue, and only when the tasks in the macro queue are finished does the next phase proceed. In the execution process, the system will constantly detect whether there are tasks to be executed in the microqueue. If there are tasks in the microqueue, the tasks in the microqueue will be executed. When the microqueue is empty, the tasks in the macro queue will be executed. This is very similar to browsers, but prior to Node 11.x, this was not the case and the microtask queue was detected after all the macro tasks in the current stage queue had been run.Copy the code
A picture is worth a thousand words: those who are interested can visit Nodejs
Verifying the Node.js environment: Who prints first?
setTimeout(() = > {
console.log('setTimeout')},0)
setImmediate(() = > {
console.log('setImmediate')})// If you run it several times, you will find that the print is random
// setTimeout setImmediate
/ / or
// setImmediate setTimeout
// This conforms to the event loop of node.js:
// Each phase has its own macro queue, and only when the tasks in the macro queue of this phase are finished, will the next phase proceed
Copy the code
So how do you do setImmediate?
The next stage we can use in the poll stage is the check stage setting:
const fs = require('fs')
fs.readFile('./async.js'.() = > {
setTimeout(() = > {
console.log(2.'setTimeout')},0)
setImmediate(() = > {
console.log(1.'setImmediate')})})Copy the code
Full version Summary
: : : tip
- Js is single-threaded, the code runs from the top down;
- The code is divided into: synchronous, asynchronous; Synchronous and asynchronous first
- Asynchrony is divided into macro task and micro task. Macro task first, micro task later (note: global Script counts a macro task)
- Relevant provisions: Whenever a piece of synchronous code is run, the code will be pushed into the call stack, and the code will be removed from the stack after the completion of the code execution. When the call stack is empty, the steps for the detection of the two task queues are as follows:
- Step 1: Check whether the microtask queue is empty. If not, remove a microtask and put it on the stack for execution, and then perform Step 1. If no, the event cycle ends and go to Step 2.
- Step 2: Check whether the macro task queue is empty. If not, pull out a macro task and execute it, and then perform Step 1. If no, go to Step 1
- … Step 1 and Step 2 Repeat
- After one round of event loop execution, the next round of event loop execution begins
UI render
UI render
It’s executed before it’s executedrequestAnimationFrame
requestAnimationFrame
After execution,UI render
Before executing, the browser decides whether to executerequestIdleCallback
: : :
check
console.log(1)
setTimeout(function () {
console.log(2)
process.nextTick(function () {
console.log(3)})new Promise(function (resolve) {
console.log(4)
resolve()
}).then(function () {
console.log(5)
})
})
process.nextTick(function () {
console.log(6)})new Promise(function (resolve) {
console.log(7)
resolve()
}).then(function () {
console.log(8)})setTimeout(function () {
console.log(9)
process.nextTick(function () {
console.log(10)})new Promise(function (resolve) {
console.log(11)
resolve()
}).then(function () {
console.log(12)})})// First loop: 1, 7, 6, 8
// Second loop: 2, 4, 3, 5
// Round 3:9, 11, 10, 12
Copy the code
Postscript (Brainstorming)
Here’s an interview question:
setTimeout(() = > {
console.log(888)},0)
Promise.resolve()
.then(() = > {
console.log(0)
return 4
})
.then((res) = > {
console.log(res)
})
Promise.resolve()
.then(() = > {
console.log(1)
})
.then(() = > {
console.log(2)
})
.then(() = > {
console.log(3)
})
.then(() = > {
console.log(5)
})
.then(() = > {
console.log(6)})// 0 1 2 3 5 6
// First loop 0 1 4 2 3 5 6
// The second loop is 888
Copy the code
Is that right? If you understand the summary above! Then I will transform diu-Diu:
setTimeout(() = > {
console.log(888)},0)
Promise.resolve()
.then(() = > {
console.log(0)
return Promise.resolve(4)
})
.then((res) = > {
console.log(res)
})
Promise.resolve()
.then(() = > {
console.log(1)
})
.then(() = > {
console.log(2)
})
.then(() = > {
console.log(3)
})
.then(() = > {
console.log(5)
})
.then(() = > {
console.log(6)})// 0 1 2 3 4 5 6
// First loop 0 1 2 3 4 5 6
// The second loop is 888
Copy the code
Is there a sense of mental implosion? If you know the answer, you can post it in the comments section
Plus events bubbling up
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
<title>Document</title>
<style>
.outer {
width: 200px;
height: 200px;
background-color: pink;
}
.inner {
width: 100px;
height: 100px;
background-color: blue;
}
</style>
</head>
<body>
<div class="outer">
<div class="inner"></div>
</div>
<script>
var outer = document.querySelector('.outer')
var inner = document.querySelector('.inner')
new MutationObserver(function () {
console.log('mutate')
}).observe(outer, {
attributes: true,})function innerClick(e) {
console.log('inner click')
setTimeout(function () {
console.log('inner timeout')},0)
Promise.resolve().then(function () {
console.log('inner promise')
})
outer.setAttribute('data-random-inner'.Math.random())
console.log('inner end')
// e.stopPropagation()
}
function outerClick(e) {
console.log('outer click', e.target)
setTimeout(function () {
console.log('outer timeout')},0)
Promise.resolve().then(function () {
console.log('outer promise')
})
outer.setAttribute('data-random-outer'.Math.random())
console.log('outer end')
}
inner.addEventListener('click', innerClick)
outer.addEventListener('click', outerClick)
inner.click()
outer.setAttribute('xueyue'.Awesome!)
outer.click()
</script>
</body>
</html>
Copy the code
I made a summary according to the browser performance: click event, without preventing bubbling: program call: will execute inner synchronization code first, then bubble out, execute outer synchronization code, empty the execution stack and then execute microtask; Belong to the same macro task. User click: will bubble out first, calculate two click events (macro task); Then execute the code.Copy the code