The article origin

Recently, some lazy, and the company has many projects, there is no time to more. This topic is rather entangled, originally wanted to continue to focus on Webpack, but in private chat with some students, I accidentally found that most of the students are not very familiar with Js running mechanism and memory mechanism. After doing some sharing, I decided to sort out the underlying foundation and principle of Js, so I have this article, mainly for the front end of the elementary and intermediate students.

What can I learn from this article?

Heap, stack, garbage collection mechanism, execution environment, execution context, execution stack, variable object, active object, lexical environment, call stack, Scope, variable promotion, single thread, multi-thread, coroutine, Event loop, browser kernel thread, event queue, microtask, macro task…..

I believe that there are still quite a few students who have only heard or understood the terms mentioned above. This article will summarize and analyze the professional terms mentioned above one by one. After reading through this article, I believe that your JavaScript foundation will be more solid.

What is a heap? What is a stack? What is the js memory mechanism?

Every language has a classification of data types, and how a language stores and allocates this data is called memory management. How do stacks and heaps relate to memory? The first thing to understand is that the stack and the heap which is stack memory and heap memory, they’re all stored in memory.

Heap memory

When we create an object or array or a new instance, we will open up a section of the heap memory for it, for storing the complex data type.

Heap memory can dynamically allocate memory size and lifetime without having to tell the compiler in advance because it allocates memory dynamically at run time, but the disadvantage is that access is slower because memory is allocated dynamically at run time.

Stack memory

A reference pointer to a simple data type and object in Js that points to the corresponding value in heap memory

Stack memory is last in first out and can be accessed directly, while heap memory is not. For our object count is a large data type and it takes up a variable amount of space, so the object is on the heap, but the object name is on the stack, so you can use the object by its name.

Garbage collection mechanism

What is garbage? When a variable is declared, whether primitive or complex, it is stored in memory, and after the code is executed, the data that is no longer used is called “garbage” waiting to be collected.

The garbage collection mechanism, which is a mechanism for how to deal with this “garbage” data, it collects it automatically.

It mainly uses the “mark clean” algorithm, garbage collection from the global point of view, those can not be reached from the “root” no longer reference (reference count is 0) data, defined as garbage data, recycling.

What is the unreachable data?

var a = 1
var b = 2
var obj = {
  c: 3.d: 4
}
Copy the code

The preceding code defines variables a, b, and obj, which are defined globally. When the script is executed, the reference count of variables a, b, and obj is 1, and garbage collection does not collect the values of a, b, and obj (because the values of a, b, and obj are accessible through the window root). And a reference to the variable obj) unless manually freed

a = null
b = null
obj = null
Copy the code

More simply, if you declare variables of primitive or complex types that can be accessed from the global Window object, the garbage collector will not collect the data.

var obj = {
  c: 3.d: 4
}
var user = obj
obj = null
Copy the code

The above code defines an object globally {c: 3, d: 4} (for convenience, we will name this object object A), it is stored in the heap, the variable obj is used to take A reference to A, the user variable is defined, and the object A is also referred to, and then the obj is re-assigned to null.

The answer is no. As mentioned above, as long as data is accessible from the global Window object, it will not be collected by the garbage collector.

How can object A be accessed globally? That is, window.user refers to object A, so it will not be reclaimed

If user = NULL is set manually, then there is no way to access the A object from the window, then the A object will be reclaimed

So, garbage collection is actually collecting data in memory, not variables, but the data that a variable points to is being collected, so what’s the use of keeping a variable? The garbage collector naturally collects useless variables as well

Now that you know the memory mechanism of JavaScript and that different variables in JS are stored in different memory, how does JavaScript parse this code and execute it? We all know that JavaScript is executed in two phases, preparse and execute, but what is the mechanism? Don’t worry, let’s talk about the js execution environment first.

JavaScript execution environment and execution stack

Execution environment (execution context)

The execution environment of Javascript is also called the execution context, which is popularly understood as the environment in which a certain piece of Javascript code is run, and who is above it? Who’s next?

Why introduce the concept of execution context? To execute the code, you first have to set rules. You first have to let me know in which environment the code I’m writing will execute and what variables will be accessible, and then I can write the correct logic.

So what exactly are the environments here? There are three types of execution context in JavaScript: global execution context, function execution context, and Eval function execution context. The specific description of the three will not be expanded in detail, many articles have detailed elaboration, here only two points.

  • There is only one global execution context and the stack is unloaded after the browser is closed.
  • Function execution contexts can be multiple, and each time a function is called, a new context is created.

Execution stack (call stack)

Careful students will notice that the global context will be off the stack after the browser is closed. Oh, I know, the stack is the stack memory, not also.

The stack, in this case, is the execution context stack, or execution stack or call stack, and it holds all the execution contexts that are created during the execution of the code. Since it is a stack, it has the characteristics of last in first out. Take a look at some code to understand the execution order of the stack in the fastest way possible:

// test.js
var foo = 1
function bar() {
  function baz() {}
  baz()
}
bar()
Copy the code
  • When this code is executed, the JS engine creates the global execution context and pushes it onto the execution stack
  • Call bar, create the execution context of bar function, push the execution stack, then Javascript execution environment is the execution environment of bar function
  • Bar function calls baz function internally, creates baz context again, and pushes it to the execution stack. At this time, Javascript execution environment is the execution environment of Baz function
  • After the baz function is executed, the execution stack pops up the execution context of baz function, and at this time the Javascript execution environment becomes the execution environment of bar function
  • After the bar function is executed, the execution stack pops up the execution context of the bar function. At this point, the Javascript execution environment becomes the global execution environment
  • I close the browser and perform stack clearing. The browser is closed, what kind of execution environment?

Ok, so now you have an idea of how the JavaScript execution context works, but you might be asking, shouldn’t it be preparsed before execution? What about the preparse step? Don’t panic, there is a way of making movies called flashbacks. Now let’s go back and see how JavaScript preparses. To understand the JS preparse process, you must first understand the execution context creation process.

Perform the process of context creation

When it comes to pre-parsing, the first thing you might think of is the promotion of variables and function declarations. Think about why they are promoted, at what stage they are promoted, and what is the mechanism of the promotion? All of these issues are explained once you understand how JavaScript performs context creation.

So you see why this article starts with the execution context?

Before we begin the process of creating an execution context, it is necessary to understand: identifiers, variable objects, live objects, lexical environments, scopes…

1. The identifier

Generally speaking, in JS, all can be named by our own can be called identifiers, such as variable name, function name, attribute name are identifiers

2. Variable objects and live objects

Variable objects and live objects are concepts in ES3. The answer is how and where we declare variables and functions in our programs, and how and where the interpreter finds our defined data.

All variables and functions inside a function are stored (implicitly) in a variable object, which is called an active object when called.

VO variable object is a special object associated with the execution context. It is used to store variables, function declarations, and function parameters in the execution context. It is a property of the execution context.

ExecutionContext = { // Execute context
    VO: {
        // Context variables}}Copy the code

The initial structure of the variable object and the name of VO vary depending on the execution context.

  • In the context of global execution, the variable object is the global object itself, and in the browser object model, the variable object in the global execution context is the Window object, which is directly accessible.
  • The execution context-VO of the function, which is not directly accessible, is known as the live object (called when the function is called).

The AO active object is referred to as the ACTIVATION Object. It stores the variables created in the function context and the Arguments object.

FunctionExecutionContext = { // Function execution context
    AO = { // Function live object
      arguments: <ArgO>}}Copy the code

3. Lexical environment

A lexical environment is a type of specification in which the lexical structure of a code defines the relationship between identifiers and specific variables and functions. That is, the lexical environment defines the mapping relationship between the identifier (variable name) and the actual variable object. Associated with the lexical environment are lexical environment components and variable environment components.

You don’t know what’s going on, do you? What the fuck is this? That’s the official definition. You don’t know why. Let me use the common grammar here to describe what a lexical environment is.

So what is the environment? Isn’t that the scope? What is scope? Isn’t that the scope? Isn’t the lexical context the lexical scope? Interpretation:

When the code is written, the scope of the variable can be determined according to the structure of the code. This scope is the lexical scope, or lexical environment.

Lexical environment is a big class, which you can think of as a parent component that has two child components: lexical environment component and variable environment component

3.1. Lexical environment components

In the lexical environment component, the main stores are function declarations and bindings that declare variables using lets and const

3.2. Variable environment components

The variable environment component stores function expressions and bindings that declare variables using var

There are two other components inside the lexical environment component and the variable environment component:

  • Environmental logger: The actual location where variable and function declarations are stored (similar to variable objects in ES3)
  • References to external environment: Parent lexical environment (similar to the [[Scope]] attribute)

4. The scope

Scope is the scope and range in which variables can be referenced and functions can take effect.

As we all know, Js is a static scope as well as a lexical scope. Once the code is written, it specifies which scope the variable or function belongs to.

We also know that Js is divided into global scope, function scope, block level scope, function level block level scope can access the global scope; In a nested function, the inner function can access the scope of the outer function… But why is it accessible?

You can explain it this way: because when javascript is written, the scope of variables can be determined according to the structure of the code, so local can access global, nested functions, internal can access external.

Isn’t it? This is just a representation. For example, in the scope of a function, in fact:

When a function is created, it creates an internal property [[Scope]] that contains variable objects in the parent context in which the function is created (in fact, the function is created in the parent execution context);

When the function is called, the execution context of the function chain contains not only the function’s active object AO, but also a copy of the function’s [[Scope]] property.

var x = 10
function foo() {
    var y = 20
}
foo()
/ / pseudo code
// 1. VO in the global execution context
// GlobalContext = {
// VO: {
// x: 10,
// this: window,
// foo: 
      
        to Function foo
      
/ /}
// }
// 2. After foo is created, the Scope attribute is
// foo.[[Scope]] = {
// VO: GlobalContext.VO,
// this: window
// }
// 3. After calling foo, create the execution context and create the live object AO
// FooContext = {
// AO: {
// y: 20,
// this: window
/ /}
// }
// foo after the execution context is created, the scope chain of the execution context is:
// FooContext.Scope = {
// AO: FooContext.AO,
VO: globalcontext.vo (that is, a copy of foo.[[Scope]])
// }
Copy the code

As mentioned above, in ES3 and ES5, the definition of a variable store is completely different. In ES5, the concept of a variable object has been replaced by a lexical environment model, so the process of creating a context in ES3 and ES5 is described separately.

The process of executing context creation in ES3

Let’s take a look at how contexts are created in ES3, using code and pseudo-code:

var x = 10;
function foo() {
  var y = 20;
  function bar() {
    var z = 30;
    alert(x +  y + z);
  }
  bar();
}
foo(); 
Copy the code
  • Open the browser to run the JS script, first create the global execution context;
    • The variable object (GlobalObject in this case) is created and initialized. The variable object in the global context is as follows:
      GlobalContext = {
          GO: {
              x: undefined.this: window.foo: <reference> to Function foo
          }
      }
      Copy the code
    • The function foo has been created at this point, so it has the [[Scope]] property (function Scope), which contains a reference to the variable object in the parent context (in this case, the global object GO), so the [[Scope]] of foo looks like this:
      foo.[[Scope]] = {
          GO: {
              x: undefined.// This is the pre-parse phase, and the execution phase will assign values
              this: window.foo: <reference> to Function foo
          }
      }
      Copy the code
  • After the global variable object is initialized, it enters the execution phase
  • The execution phase begins with an update assignment (GlobalVO) to the global variable object as follows:
    GlobalContext = {
        x: 10.this: window.foo: <reference> to Function foo} // Foo.[[Scope]] = {// GO: { // x: 10, // this: window, // foo: <reference> to Function foo // } // }Copy the code
  • Call the function foo (once the function is called, create the function execution context)
  • Start creating the execution context for foo: First, initialize variables and populate foo’s active object (AO). Second, remember the [[Scope]] property inside foo when we created it above? The [[Scope]] property inside the foo execution context copy function is stored in the Scope chain of the context and puts the AO just filled in at the top of the execution context Scope chain; The active object/variable object in the context of Foo is as follows:
    FooContext = {
        AO: {
            y: undefined.// No value is assigned during precompilation
            this: window.arguments: [].bar}, GO: {x: 10, this: window, foo: <reference> to Function foo}}Copy the code
    • At this point, the bar function has been created, so the bar function creates an internal property [[Scope]] that contains a reference to the parent context live object/variable object (in this case, the foo context live object/variable object).
    bar.[[Scope]] = {
        AO: {
            y: undefined.this: window.arguments: [].bar: <reference> to Function bar
        },
        GO: {
            x: 10,
            this: window,
            foo: <reference> to Function foo
        }
    }
    Copy the code
  • Perform the update assignment by executing the foo function
    FooContext = {
        AO: {
            y: 20.this: window.arguments: [].bar: <reference> to Function bar
        },
        GO: {
            x: 10,
            this: window,
            foo: <reference> to Function foo
        }
    }
    Copy the code
  • Call bar function
  • Before calling, create the execution context of the bar function. As with foo, the active object of bar is first placed at the top of the execution context scope chain of bar; Second, copy the bar function internal attribute [[Scope]] into the Scope chain of the execution context of the bar function. The context of the bar is as follows
    BarContext = {
        AO: {
            z: undefined.this: window.arguments: []},AO: {
            y: 20.this: window.arguments: [].bar: <reference> to Function bar
        },
        GO: {
            x: 10,
            this: window,
            foo: <reference> to Function foo
        }
    }
    Copy the code
  • Execute the bar function to perform the assignment operation
    BarContext = {
        AO: {
            z: 30.this: window.arguments: []},AO: {
            y: 20.this: window.arguments: [].bar: <reference> to Function bar
        },
        GO: {
            x: 10,
            this: window,
            foo: <reference> to Function foo
        }
    }
    Copy the code
  • In the bar function, execute alert(x + y + z)
    • X: look for the variable x in the current execution context (BarContext), found in GO, is 10
    • Y: look for the variable y in the current execution context, found in the second layer AO, is 20
    • Z: Look for the variable z in the current execution context, found in the first layer AO, which is 30
  • When the bar function completes execution, its execution context pops up the execution stack and gives execution rights back to the foo execution context
  • When foo completes execution, its execution context pops up the execution stack and the execution authority is returned to the global execution context
  • When all code is executed, close the browser and clear the execution stack

Comb through the process again:

  1. Write the functional code, open the browser to run
  2. Creating a global execution context, how? (The process of creating the context is the process of JS pre-parsing)
  • Creates and populates the variable object with its initial value, and places the variable object in the VO in the global execution context, virtually promoting the variable
  • If there is a function in the global code, the function declaration was already placed in the variable object in the previous step, that is, a function was created in the global variable object
  • Create the [[Scope]] property inside the function. The [[Scope]] property contains the variable object in the parent context, in this case the global variable object (the [[Scope]] will be created even if the function is not called).
  • The [[Scope]] property will remain in the function property until the function is reclaimed
  1. After the global execution context is created, the execution phase begins
  2. Assign a value to a variable declared using var in a global object variable
  3. When you encounter a function call, you first create the execution context for that function
  • Create an active object for the function that is populated with initial values and place the active object in the AO in the function’s execution context
  • Creates a Scope chain of the function’s execution context, consisting of the active object and the function’s internal [[Scope]] property
  1. After the function execution context is created, the function execution begins
  • Assign a value to a variable
  1. When a function completes execution, the execution context for that function pops up
  2. Back to the global execution context
  3. Close the browser and the global context pops up

Summary of key points:

  1. ES3 revolves around variable objects and live objects to create execution contexts
  2. The variable object (variable promotion) is created and populated with initial values each time it enters the context, and its update occurs during the code execution phase
  3. Js pre-parsing actually occurs in the process of creating variable objects, using initial values to fill variable objects; Also, pre-parsing only parses variables in the current context. So, when you write a program that runs in the browser, it will only parse global variables. If you don’t call a function, the code inside the function will not be preparsed!
  4. Function declarations are put into variables/active objects (VO/AO) when the context phase is entered
  5. The function life cycle is divided into the creation phase and the activation phase (call). The creation phase creates an internal property [[Scope]] that contains references to external variables
  6. The Scope chain of a function context is created when the function is called and consists of the active object and the function’s internal [[Scope]] property
  7. Js performs variable lookup through the rules of the scope chain (specifically, the scope chain of the execution context).

The process of executing context creation in ES5

The ES5 execution context creation process is similar to the ES3 process, except that the deconstruction in the execution context is different. It is divided into three main steps

  1. Binding this value
  2. Create lexical environment components (bindings for lets and const variables, function declarations)
  3. Create variable environment components (var variable bindings and function expressions)

As mentioned above, lexical environment components and variable environment components are lexical environments, and they both have environment loggers and references to external environments

Rather confusing, you can simply think of the environment logger as a variable object or live object in ES3, and the reference to the external environment as a scope in the execution context. Why is that? First, there are two kinds of environment loggers: declarative environment loggers and object environment loggers, which can be corresponding to live objects and variable objects respectively. In other words:

In the global context, ES5 uses object environment loggers to store variables and functions, and ES3 uses variable objects to store variables and functions

In a functional environment, ES5 uses declarative environment loggers to store variables and functions, and ES3 uses live objects to store variables and functions

The execution context in ES5 is expressed in pseudocode as follows:

ExecutionContext = {
    ThisBinding = <this value>, // LexicalEnvironment = {// environment logger, which stores variables declared in the context by let const, and function declarations // Can be understood as variable objects or live objects EnvironmentRecord depending on the context type: {}, // the current context refers to the outer environment, and scope outer:<>}, VariableEnvironment = {// environment logger, which stores variables declared by var in the context, and function expressions // Can be understood as variable objects or live objects EnvironmentRecord depending on the context type: {}, // the current context refers to the outer environment, and scope outer:<>}},Copy the code

In addition to creating context structures, ES3 and ES5 both parse and execute js programs the same way, so if you understand ES3’s context creation process, you should be able to understand ES5 as well.

In fact, looking closely at the code above, it is not difficult to see that the bar function is a closure, but we did not analyze it. Let’s take a short look at it to understand how closures work

// Change the code slightly to set the return value of function foo to function bar
var x = 10;
function foo() {
  var y = 20;
  function bar() {
    var z = 30;
    alert(x + y + z);
  }
  return bar
}
var fn = foo()
fn()
Copy the code

What is a closure? If you declare a function globally and use a global variable inside the function, that function is a closure, but because it is a global function, and global variables can be referenced anywhere, so the closure is a bit large, “package” contains all the variables in the global.

In the above code, foo is a closure that references a global variable, although it is a bit odd to call it a closure if it is global. So, what we think of as a closure is: if the inner function references the variables of the outer function, then the inner function is the closure, and the “package” is filled with the variables of the outer function. This is easy to understand by referring to the steps we took to analyze the context creation process in ES3 above

  1. Create a global context, initialize variables, and populate the initialized variables in the variable object of the global context;
// This is the global context variable object
{
    x: undefined.foo: Function foo() {},
    fn: undefined
}
// This step creates the foo function
// Create an internal attribute for the foo function [[Scope]]
// The [[Scope]] property contains a reference to the global variable object (a closure)
Copy the code
  1. After the creation, execute the code and assign the variable
    • x = 10
    • Call foo and assign the return value to the fn variable
  2. The function foo was called in the previous step, so to create the context for the execution of the function foo, skip the initialization step and go straight to the execution phase
// Foo is executed in the following context:
FooContext = {
    AO: { 
        y: 20.bar: Function bar() {}
    },
    VO: { // Global variable object
        x: 10.foo: Function foo() {},
        fn: Function bar() {},
    }
}
// For the same reason, when the context foo is created, the bar function is created
// The internal property of the bar function [[Scope]] is created, which contains the live object and global variable object in the context of foo
/ / :
bar.[[Scope]] = {
    AO: { 
        y: 20.bar: Function bar() {}
    },
    VO: {
        x: 10.foo: Function foo() {},
        fn: Function bar() {},
    }
}
Copy the code
  1. The foo function returns the bar function. When foo is finished, the foo context is off the stack and returned to the global context
  2. The context is now the global execution context, so the function foo no longer exists, and neither does the variable y inside foo and the function bar. Instead, the function definition of bar is assigned to the global fn variable.
    • You might ask, “Why does the function bar still refer to the y variable when it doesn’t exist?” This is where closures come in
  3. Remember the [[Scope]] attribute when the bar function was created? It has live objects for the parent function foo and variable objects in the global
  4. When fn is executed, the execution context of fn is created, and the execution context copies the [[Scope]] property of the function defined by bar into that context. Then we initialize the variable z inside the function.
// Fn is executed in the following context
FnContext = {
    AO: { // The live object defined by the bar function
        z: 30
    },
    AO: { // The active object of the foo function
        y: 20.bar: Function bar() {}
    },
    VO: {
        x: 10.foo: Function foo() {},
        fn: Function bar() {},
    }
}
Copy the code
  1. In the execution phase, alert(x + y + z) looks for variables x, y, z along the scope chain in the current execution context, and outputs as 10,20,30

Note: In the above code: Var fn = foo() assigns the return value of foo to the variable fn, because there is a reference, that is, fn refers to the return value of foo, which is the definition of bar, so even if fn is not called, The definition of the function and its closure (the variables inside foo) will always exist in memory and will not be collected by garbage collection. Only when an object or function does not have any reference variables refer to it will the system’s garbage collection mechanism reclaim it upon verification. So use closures with caution to avoid memory leaks!

A quick tip: If you don’t understand the process of creating an execution context in ES5, you can first see the process of creating a context in ES3, which is nothing more than a different storage structure for variables. The principle is similar, as long as you understand that the process of creating a context is actually a process of parsing and a process of generating a scope chain.

Processes and Threads

Confused about processes and threads? Never mind, old rule, definition:

  • A process is the basic unit by which the operating system allocates resources and schedules tasks
  • A thread is a unit of program running at a time based on a process. A process can have multiple threads.

Aside from the official boring definition, the common definition of process and thread, take the example of drilling trolley, drilling trolley can be divided into one-armed and multi-armed, above:

Redefine processes and threads

  • Processes are the number of drill carts you can take out, if you can take out one, it’s single process, if you can take out multiple, it’s multiple processes
  • Threading is based on the type of drill bed, are you one-armed or multi-armed, if one-armed, it’s single-threaded, multi-armed is multi-threaded

Is that clear? As we all know, JS is a single threaded language, that is, one-armed, can only do one thing at a time; Browsers are multi-process and multi-threaded, so you can open multiple browser tabs at once, which is the embodiment of multi-process; Each browser TAB (browser kernel) is multi-threaded, which is divided into GUI rendering thread, JS engine thread, event trigger thread, timing trigger thread, asynchronous HTTP request thread. For the specific thread, we will be in detail when the js execution mechanism.

coroutines

The introduction of coroutines here refers to the introduction of Generator functions in ECMAScript 6. Generator functions are used to better control the flow of asynchronous tasks, specifically sending them to the portal

Since the coroutine is not very obvious except in ES6, I will not expand it here, and I will consider writing a special article on coroutines later.

JS execution mechanism

For javascript code to run, it must go through two phases: pre-parsing and execution.

In the previous article, we spent a lot of time to talk about the js execution context and its creation process. In fact, the process of context creation can be fully understood as the JS parsing mechanism. After parsing, the context is created OK, and then the code needs to be executed.

You might say, well, how else do you do it? Do it from top to bottom.

No, there are many different kinds of JS code, such as synchronous, asynchronous, timer, binding event, Ajax request, and so on. The execution mechanism of JS stipulates the execution order of the code, so as to ensure that a single threaded language will not block because of a certain code execution time is too long. This execution mechanism is the event loop of JS.

In fact, in explaining the concept of event loops, many articles have mentioned synchronous and asynchronous tasks, microtasks and macro tasks. Combined with the code, the basic analysis is as follows:

console.log(1) / / synchronize
setTimeout(function() {/ / asynchronous
    console.log(2)},10)
console.log(3) / / synchronize
// In the stack, synchronization is executed first, output 1
// The callback function of the asynchronous task is added to the task queue.
// Then synchronize the task, output 3
// The synchronous task has completed, if the asynchronous task has results (after 10ms)
// Add the async callback function from the task queue to the execution stack, output 2
// Result: 1 3 2
Copy the code

But once Promise and Process. nextTick were added, the picture changed

console.log(1) / / synchronize
setTimeout(function() {/ / asynchronous
    console.log(2)},10)
new Promise((resolve) = > {
    console.log('promise') / / synchronize
    resolve()
}).then((a)= > {
    console.log('then') / / asynchronous
})
console.log(3) / / synchronize
// In the stack, synchronization is executed first, output 1
// The callback function of the asynchronous task is added to the task queue. (console.log(2) and console.log('then'))
// Then synchronize the task, output promise and 3
// The synchronous task has completed, if the asynchronous task has results (after 10ms)
// Add the async callback function from the task queue to the stack and execute, output 2 then
// Promise 3 2 then
// Correct output: 1 promise 3 then 2
Copy the code

Was it right? The two asynchronous tasks console.log(2) and console.log(‘then’) are executed in the wrong order. 2 go first, then go in, why the execution result is reversed? They are both asynchronous tasks, and the synchronous and asynchronous explanations alone make a difference.

As a result, many articles naturally lead to microtasks and macro tasks

Macro task: overall code script, setTimeout, setInterval

Microtasks: Promise, Process. NextTick

The execution mechanism becomes: first execute the overall code (macro task), after the execution is complete, execute the microtask, the microtask is completed, the first round of event cycle is completed; Start the second round: read the task queue to see if there are macro tasks. If there are macro tasks, run……

In the above code, since the promise is a microtask, it takes precedence over setTimeout.

It’s not wrong, it’s right, and I’m not trying to argue with them here. But here’s the puzzle:

  1. What is a task queue?
  2. How do you define macro tasks and microtasks?
  3. How do I know that the overall code script is a macro task and the Promise is a microtask? All from memory?
  4. Is asynchronous Ajax a microtask or a macro task?
  5. Is the callback to the user click event a microtask or a macro task?

So many questions, no matter, let me take you to crack one by one. First, it is necessary to understand the browser kernel.

Browser kernel threads and event queues

When we introduced processes and threads we talked about how the browser kernel is multi-threaded, which includes the following threads:

  • GUI rendering thread: mainly used for DOM tree parsing, rendering, redrawing (DOM related), etc
  • JS engine thread: executes javascript scripts
  • Event-triggering thread: responsible for managementThe event queueAnd hand over to the JS engine thread for execution. Events such as DOM binding (onclick, onMouseEnter, etc.), the end of the timer, the end of the request, and when the conditions are met, the corresponding callback function is added toThe event queue(that is, the task queue).
    • Event queue: mainly for different events (timer, user trigger events, request events) of the callback function, when reached the corresponding conditions (such as timers, users click on the button, the request is complete and response), after the event thread, the callback function will meet the conditions, added to the task of js engine threads for waiting in the queue.
  • Timer thread: the thread where setTimeout and setInterval are located, mainly timing the timer. When the time is up, the event-triggered thread adds the callback function to the event queue
  • Asynchronous network request thread: An asynchronous request is made within this thread. After the request state changes, if there is a callback, the event-triggered thread adds the callback processing to the event queue

In conjunction with the browser kernel, let’s take a look at what microtasks and macro tasks are. Here, again, is a part of the article that describes microtasks and macro tasks:

Macro task: overall code script, setTimeout, setInterval

Microtasks: Promise, Process. NextTick

Macro task

First of all, macro tasks and microtasks are asynchronous tasks

The so-called macro task is the task that needs to be processed by other threads, the other threads here include JS engine thread, event trigger thread, timer thread, asynchronous network request thread, so once it is linked to the above four threads, it is a macro task. Let’s take the macro tasks listed above and explain them:

  • Overall code script (JS engine thread)
  • SetTimeout, setInterval (timer thread)
  • Ajax asynchronous request (asynchronous Network request thread)
  • Onclick event binding (event-triggering thread)

Are there more macro tasks than those given above? So, remember: any task that involves a browser kernel thread is a macro task.

Micro tasks

Microtasks are usually tasks that need to be executed immediately after the current task is finished, such as asynchronous tasks that do not need to be processed by the JS engine threads (except words are critical!). .

First, it is asynchronous, and second, it does not require such things as event-triggered or timer threads or asynchronous network request threads to process. Take a look at the Promise, process. NextTick, is there a specific kernel thread in the browser to handle it? You might say it’s executed by the JS engine thread, and it is, but it’s asynchronous, so it’s a microtask.

You can think of it this way: tasks that are either macro tasks or microtasks.

Event loop

After a lot of talking, we finally come to the event cycle. With the previous theory, the event cycle is very easy to understand.

Again, event loops are Javascript’s execution mechanism, that is, they dictate the flow of Javascript code execution:

  1. Perform the whole paragraphJavaScriptCode, put the synchronization task in the execution stack (said above, this is a macro task)
  2. In the code if there issetTimeoutajaxAnd so on macro task, will use the corresponding browser kernel thread to process, reach the condition (timer time reached, the request is complete), byEvent-triggered threadAdd its corresponding callback toEvent queue (task queue)
  3. If you havePromiseAfter the execution stack completes the current synchronization task, take out the microtask from the microtask queue and execute it immediately
  4. After all microtasks are executed, the execution stack is in idle state (Note: after all microtasks are executed in this round of event loop, the next round of loop is opened).
  5. That’s the first round of the event cycle. Here’s the second round:
  6. Event queues add queued tasks to the execution stack and execute them in first-in, first-out order
  7. If the stack contains a synchronization task, a microtask, and a macro task, execute the synchronization task first
  8. Then perform all the microtasks
  9. The second cycle of events ends, and the third cycle begins:
  10. Execute macro tasks… The loop repeats until all tasks are completed.

The following three pieces of code from the shallow to the deep analysis:

Case 1: Only macro tasks
console.log(111)
setTimeout(function() {
    console.log(222)},5000)
$.ajax({
    url: ' '.success: function() {
        console.log(333)}})console.log(444)
Copy the code
  1. To run the code, synchronize the entire piece of code firstconsole.log(111), the output 111
  2. encountersetTimeoutAnd give it toTimer threadTo deal with
  3. encounterajaxRequest, give it toAsynchronous network request threadsTo deal with,
    • Note: There is only one task in the event queue:Ajax successful callback(Provided that the response time from start to end of the request is less than 5 seconds, the following default is not explained)
    • Yi? Why is that? In fact, as I said earlier,setTimeoutIs in theTimer threadIt takes about 5 seconds to put it in the event queue, and at this point, ajax is executing very fast, less than 5 seconds, sosetTimeoutThe callback for is not yet added to the event queue
  4. Execute synchronization codeconsole.log(444), the output 444
  5. Because this code has no microtask, the first round of the event loop is over and the second round of the event loop is ready to start
  6. The execution stack is empty and the task needs to be fetched from the event queue
    • Notice at this point that there may not be any tasks in the event queue
    • becauseTimer threadFive seconds later, before theEvent-triggered threadGive its callback function to the event queue
    • At the same time,Asynchronous network request threads, after the request has completed the response 200, byEvent-triggered threadGive its successful callback function to the event queue
    • So, if the request takes too long, or if the code took less than 5 seconds before execution, the event queue could be empty
  7. Suppose the Ajax request time is 2 seconds, and the event queue is thereajaxSuccessful callback, take out the callback, output 333
  8. After 5 seconds,Timer threadwillsetTimeoutIs added to the event queue
  9. Because the execution stack is empty, the event queue is fetched directlysetTimeout, output 222
  10. Results: 111 444 333 222
  11. If the Ajax request time is longer than 5 seconds of the timer, the result is: 111 444 222 333
  • Since it is not good to limit the request time, let’s set the setTimeout timing to 0
  • SetTimeout set to 0 means that when you run your code, once you get to the setTimeout line,Timer threadImmediately theSetTimeout callbackPut into the event queue and take out for execution as soon as the execution stack is free.
  • At this point, there is no problem with Ajax and setTimeout. In the event queue, the Ajax callback will always be after the setTimeout callback
Case 2: Macro tasks and Microtasks (1)
console.log(111)
setTimeout(function() {
    console.log(222)},0)
new Promise((resolve) = > {
    console.log('333')
    resolve()
}).then((a)= > {
    console.log('444')
})
$.ajax({
    url: ' '.success: function() {
        console.log(555)}})console.log(Awesome!)
Copy the code
  1. To run the code, synchronize the entire piece of code firstconsole.log(111), the output 111
  2. encountersetTimeoutAnd give it toTimer threadProcessing, 0 seconds before theEvent-triggered threadGive its callback function to the event queue
  3. encounterPromise, Promise executes the function immediately, so output 333
  4. It has onethenThe callback, which is a microtask, will not be executed
  5. encounterajaxRequest, give it toAsynchronous network request threadsProcessing, request completion response by 200Event-triggered threadGive its successful callback function to the event queue
    • At this point, assume that the Ajax request is complete, so the event queue is now ①SetTimeout callbackAnd (2)The ajax callback
  6. Execute synchronization codeconsole.log(666), the output 666
  7. After the synchronization task is completed, check whether any microtask exists. If yes, go tothenCallback to start execution, output 444
  8. With no other microtasks left, the execution stack is empty, the first round of the event loop is over, and the second round of the event loop is ready to start
  9. Get the task from the event queue
  10. First of all take outsetTimeout, output 222
  11. Then remove themajaxTo output 555
  12. Results: 111 333 666 444 222 555
Case 3: Macro and Micro Tasks (2)
console.log(111)
new Promise((resolve) = > { // promise1
  console.log(222)
  new Promise((resolve) = > { // promise2
    console.log(333)
    resolve()
  }).then((a)= > {
    console.log(444)
  })
  resolve()
}).then((a)= > {
  console.log(555)
  new Promise((resolve) = > { // promise3
    console.log(Awesome!)
    resolve()
  }).then((a)= > {
    console.log(777)
  })
})
setTimeout(function() {
  console.log(888)},0)
console.log(999)
Copy the code
  1. Execute code, the current execution stack is the global execution context stack, execute the first line of codeconsole.log(111), the output 111
  2. If promise1 is encountered, enter 222
  3. Promise1 also contains PromisE2, which is executed immediately, outputs 333, and adds promisE2’s THEN callback to the microtask queue
  4. Promise1’s THEN callback is added to the microtask queue, which follows promisE2’s THEN callback
  5. If you encounter a setTimeout, give it toTimer threadProcessing, 0 seconds before theEvent-triggered threadGive its callback function to the event queue
  6. Execute the last line of code, printing 999
  7. After all synchronization tasks are executed, there are no tasks left on the execution stack. Are there any microtasks? Yes, there are two microtasks in the microtask queue and they are retrieved in turn
  8. Take out the first microtask, then callback for PROMISe2, add to the execution stack, output 444
  9. Take out the second microtask, as the THEN callback of PROMISE1, add it to the execution stack, and output 555. At this time, the THEN callback of PromisE1 contains PromisE3 again, execute Promise3, output 666, and add the THEN callback of PromisE3 to the microtask queue
  10. The execution stack task is empty. Are there any tasks in the microtask queue? Yes, call back for “then” for “promise3”
  11. Select * from ‘then’ where ‘then’ is added to ‘execute stack’ and ‘777’ output
  12. The microtask queue is empty, the first round of the event loop is completed, and the second round of the loop is started to fetch tasks from the event queue
  13. Execute the callback for setTimeout, output 888, there are no more tasks, the event loop ends
  14. Results: 111 222 333 999 444 555 666 777 888
Case 4: Macro tasks and microtasks nest each other
console.log(000)
setTimeout(function() {
  console.log(111)
  setTimeout(function() {
    console.log(222)
    new Promise((resolve) = > {
      console.log(333)
      resolve()
    }).then((a)= > {
      console.log(444)})},0)},0)

new Promise((resolve) = > {
  console.log(555)
  resolve()
}).then((a)= > {
  new Promise((resolve) = > {
    console.log(Awesome!)
    setTimeout(function() {
      resolve()
    }, 0)
  }).then((a)= > {
    console.log(777)
    setTimeout(function() {
      console.log(888)},0)
  })
})

$.ajax({
  url: ' '.success: function() {
    console.log(999)}})console.log(1010)
Copy the code

Is it crazy? What the hell is this? Who writes code like this? Sure, it’s easy to get angry, but if you can analyze this code completely, you’ll be fine with the js implementation mechanism. Now start the analysis, get ready to mentality ~

  1. Execute code, the current execution stack is the global execution context stack, execute the first line of codeconsole.log(000), 0
  2. Encounter the firstsetTimeout(here it’s called the setTimeout1 callback)Timer threadProcessing, 0 seconds before theEvent-triggered threadGive its callback function to the event queue
    • The current event queue contains: ①SetTimeout1 callback
  3. encounterPromise, immediately execute, output 555, itsthenThe callback is a microtask and is not executed
  4. When you encounter Ajax, hand it over toAsynchronous network request threadsProcessing, assuming the request takes 10 seconds, 10 seconds later, byEvent-triggered threadGive its successful callback function to the event queue
  5. Execute the last line of synchronization codeconsole.log(1010), the output 1010
  6. After the synchronized code is executed, are there any microtasks? Yes, promisethenCallback function to execute
  7. thethenThe callback contains one morePromiseTo performPromise, outputs 666, whichthenThe callback is a microtask and is not executed
  8. And then you see thePromiseAlso containssetTimeout(here it’s called the setTimeout2 callback), so give it toTimer threadProcessing, 0 seconds before theEvent-triggered threadGive its callback function to the event queue
    • The current event queue contains: ①SetTimeout1 callbackAnd (2)SetTimeout2 callback
  9. Because of itsresolveIn setTimeout, therefore, this round of microtasks does not execute thisthenThe callback
  10. Are there microtasks? No, the global execution context stack is empty. The first round of event cycle is over, and we are ready to start the second round of event cycle
  11. The task is fetched from the event queue. The current event queue contains: ①SetTimeout1 callbackAnd (2)SetTimeout2 callback
  12. First theSetTimeout1 callback, first output 111
  13. It also has insidesetTimeout(here it’s called the setTimeout3 callback)Timer threadProcessing, 0 seconds before theEvent-triggered threadGive its callback function to the event queue
    • The current event queue contains: ①SetTimeout2 callbackAnd (2)SetTimeout3 callback
  14. After setTimeout, any microtasks? No, the global execution context stack is empty. The second round of event loop is over and the third round of event loop is ready to begin
  15. The task is fetched from the event queue. The current event queue contains: ①SetTimeout2 callbackAnd (2)SetTimeout3 callback
  16. First theSetTimeout2 callbackWhat’s the callback function that executes setTimeout2? Remember from the first round of the event loopresolve?
  17. performresolve, that is, the PROMISE’s THEN callback, which outputs 777
  18. It also has insidesetTimeout(here it’s called the setTimeout4 callback)Timer threadProcessing, 0 seconds before theEvent-triggered threadGive its callback function to the event queue
    • The current event queue contains the following: (2)SetTimeout3 callbackAnd (4)SetTimeout4 callback
  19. So, setTimeout2, any microtasks? No, the global execution context stack is empty. The third cycle of events is over and we are ready to start the fourth cycle
  20. First theSetTimeout3 callback, first prints 222
  21. SetTimeout3 has a Promise inside, executes, and outputs 333
  22. Are there microtasks? Yes, then callback for promise, execute, output 444
  23. There are no more microtasks, and the global execution context stack is empty. The fourth cycle of events is over and we are ready to begin the fifth cycle of events
  24. First theSetTimeout4 callback, execute, output 888
  25. Are there microtasks? No, the global execution context stack is empty. The fifth round of the event loop is over
  26. After 10 seconds, there is an event queueajaxThe callback
  27. The global execution context stack is empty, so fetch it immediatelyajaxTo execute, output 999

The results were 0, 555, 1010, 666, 111, 777, 222, 333, 444, 888, 999

If you don’t get it, that’s normal. One, because the code is so sick that nobody writes it. Two is indeed my analysis is a little verbose, no way, have tried their best to simplify the analysis. However, I really suggest that you students can carefully analyze the execution order of it, research understand, JS implementation mechanism will be thoroughly understood.

Personally feel still have a little regret, did not explain JS execution mechanism and execution stack correlation, the last case below, let us thoroughly JS execution mechanism and context together for analysis.

<script>
console.log(111)
function foo() {
  new Promise(function(resolve) { // Promise1
    console.log(222)
    resolve()
  }).then(function() {
    console.log(333)
  })
  setTimeout(function() { // setTimeout1
    console.log(444)
  }, 0)
}
foo()
new Promise((resolve) => { // Promise2
  console.log(555)
  resolve()
}).then(() => {
  console.log(666)
})
setTimeout(function() { // setTimeout2
  console.log(777)
}, 0)
console.log(888)
</script>
Copy the code
  1. With all the code running in the browser, the global context stack is created, which I won’t repeat.
  2. After the creation is complete, execute the synchronization task in the global context stack, and output 111
  3. Upon a call to function foo, start creating the foo function execution context stack
    • The execution stack is: [foo function stack, global stack]
  4. In the foo function execution context stack, Promise1 is encountered, and its first argument is immediately executed, and the argument is an anonymous function
  5. Create anonymous function execution context stack (here called anonymous stack 1), executeconsole.log(222).The output of 222; Resolve () : change the state of the Promise object from “incomplete” to “successful”, so change the state of the Promise object from “incomplete” to “successful”console.log(222)Resolve is called immediately after execution.
    • The execution stack is: [anonymous stack 1, foo function stack, global stack]
  6. Call resolve and create an anonymous function execution context stack (here called anonymous stack 2). At this point, the program jumps to the THEN method. Since the Promise’s THEN method is a microtask, it will not be executed at this time and will be placed in the microtask queue.
    • The execution stack is: [anonymous stack 2, anonymous stack 1, foo function stack, global stack]
  7. The anonymous function created when the resolve call executes the context stack out
    • The execution stack is: [anonymous stack 1, foo function stack, global stack]
  8. The first anonymous function in Promise1 has no other code to execute, so the first anonymous function in Promise1 has no other code to execute
    • The execution stack is: [foo function stack, global stack]
  9. At this point, the execution context stack is the foo stack, and there is a setTimeout inside the foo function, which is handed over (setTimeout1)Timer threadProcessing, 0 seconds before theEvent-triggered threadGive its callback function to the event queue
  10. After the foo function is executed, the foo function is off the stack
    • The execution stack is: [global stack]
  11. Create anonymous function execution context stack (anonymous stack 3)
    • The execution stack is: [anonymous stack 3, global stack]
  12. In the current anonymous stack 3, executeThe output of 555, calls resolve (again, not writing the stack change in detail), and puts its then callback on the microtask queue
    • [Promise1 then, Promise2 then]
  13. Anonymous function execution, anonymous stack 3 out of the stack
    • The execution stack is: [global stack]
  14. If I go back to the global stack, I run into setTimeout2, and I give it toTimer threadProcessing, 0 seconds before theEvent-triggered threadGive its callback function to the event queue
    • The event queue is now: [setTimeout1 callback, setTimeout2 callback]
  15. Execute the last line of code, output 888
  16. Global stack synchronization code execution is completed, take out the microtask queue execution
    • The microtask queue is [THEN for Promise1, then for Promise2], which is based on the first-in, first-out principle
  17. (1) create a stack of anonymous functions (anonymous stack 4);The output of 333
    • The execution stack is: [anonymous stack 4, global stack]
  18. [global stack] [global stack] [global stack] [global stack]
  19. (2) Create anonymous function execution context stack (anonymous stack 5)The output of 666
    • The execution stack is: [anonymous stack 5, global stack]
  20. The execution stack is [global stack] and the microtask queue is empty. The first round of event loop ends and the second round begins
  21. Retrieve tasks from the event queue. The event queue is [setTimeout1 callback, setTimeout2 callback], which is based on the first-in, first-out principle
  22. The setTimeout1 callback is executed first. Again, create anonymous function stack 6, execute output 444, anonymous stack 6 out of the stack, back to the global stack
  23. Again, the setTimeout2 callback is executed, again, creating the anonymous function stack 7, executing the output 777, anonymous stack 7 out of the stack, back to the global stack
  24. Results: 111 222 555 888 333 666 444 777

Summary ending

  • Whatever the code is, whether it’s asynchronous, whether it’s mouse events, sort them out, put microtasks in microtask queues, macrotasks handled by the corresponding thread, put them in event queues;
  • After the synchronization task is completed, the microtask queue is fetched and all microtasks are executed.
  • No more microtasks, execute the code in the event queue;
  • Microtask queue and event queue, adhering to the first-in, first-out principle;
  • If a function is called and there is a microtask or macro task inside the function, the microtask or macro task is executed in the global execution context stack

Review the knowledge points covered in this article

Heap, stack, garbage collection mechanism, execution environment, execution context, execution stack, variable object, active object, lexical environment, call stack, Scope, variable promotion, single thread, multi-thread, coroutine, Event loop, browser kernel thread, event queue, microtask, macro task…..

If you had the patience to see this, it was not easy. I admit that it does write some wordy, wordy reason is, want to take more care of the first front end, or the foundation is not solid students. If you have fully grasped the principle of it, you can also skip through it to see if my explanation is the same as yours, and also very much hope that you can put forward valuable suggestions, there are mistakes in the article, welcome to point out, more learning communication.