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 outThe 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 disorderlykey-value Key/value pairStorage 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 outData 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
  • typeofThe operator can determine the original type of a value, whileinstanceofThe 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 segmentationGlobal context,Function contextandBlock-level context
  • Each time the code execution flow enters a new context, one is createdThe scope chain, used to search variables and functions
  • Of a function or blockLocal contextNot only can you access variables in your own scope, but you can also access any variableInclude contextAnd evenGlobal contextThe variables in the
  • The global context is accessible onlyGlobal contextCannot directly access any data in the local context
  • The execution context of a variable is used to determine whenFree memory
  • JavaScriptIs a programming language that uses garbage collection, so developers don’t have to worry about memory allocation and collection.JavaScriptThe 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.JavaScriptThe engine no longer uses this algorithm, but some older versions of IE are still affected by this algorithm becauseJavaScriptWill access non-nativeJavaScriptObjects, 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 VolumeChapter 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

  1. JavaScriptCode execution process, in addition to rely onExecution stackIn addition to determining the order in which functions are executed, it also depends on the task queuetask queueTo getAsynchronous codeThe execution.
  2. Event loops are unique within a thread, but task queues can have multiple
  3. The task queue is subdivided intomacro-task(macro task) andmicro-task(Microtask), inECMAScript,macro-taskReferred to astask,micro-taskReferred to asjobs
  4. macro-taskThese include:Script (whole code).setTimeout.setInterval.setImmediate.I/O.UI rendering
  5. micro-taskThese include:process.nextTick.Promise.MutationObserver
  6. setTimeout/PromiseAnd so we call it the task source. What enters the task queue is the specific execution task they specify
  7. Tasks from different task sources enter different task queues. Among themsetTimeoutsetIntervalIs the homologous
  8. Each of these tasks is performed, bothmacro-taskormicro-task, with the help ofExecution stackTo 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.

  1. It is fromScript (whole code)Start the first loop. The global context then enters the execution stack. untilExecution stackempty
  2. And then execute all of themmicro-task. When all executablemicro-taskAfter execution, the loop ends.
  3. The next cycle starts again frommacro-taskStart, findmacro-taskThe first task in theExecution stack.
  4. Then do it againmacro-taskAfter the completion of the synchronization code, execute all of the macro tasksmicro-task.
  5. 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:

  1. 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

  2. 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;

  3. 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 meetsetTimeout, know it is a macro task in asynchronous code, turnfunction () { console.log('setTimeout') }Put into the macro task queue
  • Then meetPromise.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 timeScript (whole code)After the command is executed, a dialog box is displayedExecution stackThen 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 queueExecution stack
  • Execute code, printpromise1, implicitreturn undefinedEncountered,.thenAdd one to the microtask queuefunction () { console.log('promise1') }And then out of the stack;
  • Keep checking to see ifMicro tasksIf yes, add itExecution stack.
  • Execute code, printpromise2Continue to check if there isMicro tasks, find no, end of this cycle.
  • Operations before the next cycle begins: Check whether toTo renderPage to determine whether to executerequestAnimationFrame requestIdleCallback UI render; After the
  • Start the next cycle, checkMacro task queueIn, 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, executeOperations 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

  1. Js is single-threaded, the code runs from the top down;
  2. The code is divided into: synchronous, asynchronous; Synchronous and asynchronous first
  3. Asynchrony is divided into macro task and micro task. Macro task first, micro task later (note: global Script counts a macro task)
  4. 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:
  5. 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.
  6. 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
  7. … Step 1 and Step 2 Repeat
  8. After one round of event loop execution, the next round of event loop execution beginsUI render
  9. UI renderIt’s executed before it’s executedrequestAnimationFrame
  10. requestAnimationFrameAfter execution,UI renderBefore 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