Processes and threads
To understand how JS works, we first have to have some prior knowledge, that is, the concept of processes and threads. Here is an excerpt from Baidu Baike
- Process: Process is a running activity of a program in a computer on a data set. It is the basic unit of system resource allocation and scheduling, and the basis of operating system structure. In the early process-oriented computer architecture, the process is the basic execution entity of the program. In modern thread-oriented computer architectures, processes are containers for threads. A program is a description of instructions, data and their organizational form, while a process is an entity of the program.
- Thread: The smallest unit in which an operating system can schedule operations. It is contained within the process and is the actual operating unit within the process. A thread is a single sequential flow of control in a process, and multiple threads can be concurrent in a process, each performing a different task in parallel.
For the above concept, it is a bit difficult to understand, after looking up some metaphors and descriptions of processes and threads on the Internet, I think it should be understood in this way.
- The inclusion relationship is that a process contains a thread.
- We regard CPU as an Arsenal, there are many workshops in this Arsenal, the task of each workshop is different, some make cannons, some make tanks, and some make planes…… Therefore, we can regard each workshop as a process. Assuming that our computer has a single-core CPU, the power of our factory is limited, and only one workshop can operate, while the other workshops can only operate after the tasks of the current workshop are completed and the power is released. The summary is that only one process can be running on each CPU at a time.
- For our normal operation of the process, that is, we compare a workshop, in this workshop, there are many workers, each worker is responsible for specific tasks are not the same, some workers are responsible for steel, some workers are responsible for casting steel, some workers are responsible for ammunition…… A worker in a workshop is like a thread in a process, but because workers are all in the same workshop, using the tools in the workshop to produce, for our thread, all threads share the same process’s resources, such as memory, etc.
JS runtime mechanism
So from the above we know roughly what is the process and thread, discuss JS now, first of all, we should all know JS is a single-threaded explanatory language, single thread means that the task of JS always run in one thread, although there is a woker class implements the so-called “multi-threaded” but is still on the ground floor in a single thread. As for what is an interpreted language, JS is not compiled into a computer executable language and then executed, like C++ and JAVA. It is dynamically compiled, that is, executed as it is compiled. Who does this compilation work: the browser kernel or V8 engine (chrome and NodeJS). Who provides the specific functions, such as manipulating DOM objects and retrieving information about the computer’s operating system, that are programmed into computer-executable statements? The answer is the environment. The following figure describes the general process of JS to the implementation of specific functions.
Through this picture we have a general understanding of the running mechanism of JS, now let’s describe the JS engine and running environment in detail.
JS engine
The V8 engine is the javascript engine of the current Chrome browser as well as the NodeJs engine. So who is this engine? It’s just a heap and a stack. It’s a memory heap and a call stack, and some of the descriptions of the stack you can look at the descriptions of the stack at the end of JavaScript asynchrony. So the following is a model of the JS engine
In the call stack of JS, namely the implementation of each function, these functions include global function, usually in the sense of function and the eval function, after get the JS code, global code to the bottom of the stack into the first, and then meet the function in the code will be into the stack and perform the functions, basic types of variables are directly, existing in the stack, The actual value of a reference type is stored in the heap, and its address in the heap is stored on the stack. In the call stack, where the actual code logic is executed, the code is divided into executable units that are pushed and executed one by one. These executable units are called “execution contexts,” which I will describe in detail in a separate article later. So how does the execution stack execute code? The global code is first pushed onto the stack, and when it is executed, it encounters another function, and the function is pushed onto the stack and executed. If the function is found to have called another function, the function is pushed onto the stack again, and no more function is called, and the global code is unpushed after execution. The following code is an example
function a () {
console.log('this is print')}function b () {
a()
}
b() // this is print
/ /... Other code
Copy the code
The execution in the call stack is as follows
- So the global code is pushed as a function, so I’m going to call it the global function, and there’s a function at the bottom of the call stack called global()
- A call to b() is found in global(), at which point b() is pushed onto the stack
- A call to A () is found in b(), at which point a() is pushed
- The console.log() function is called in a() and pushed
- There are no other functions in a(), so start executing console.log() and unstack it when it’s finished
- There is no other function in b(), which unstacks a()
- In global(), b() has finished executing, unstack it
- At this point, the code in this example completes execution.
The running process is shown in the figure below:
In fact, the above is just a relatively simple way to describe the JS function in the call stack execution, we will explain the JS engine specific execution process.
JS engine execution process
The JS engine executes in three phases:
-
Grammatical analysis stage
This stage is the earliest execution after the LOADING of THE JS code, in order to check the syntax error of the JS code. If any syntax error is detected, a syntax error will be thrown. If the check is complete and there is no error, the pre-compilation stage will be entered. Like this code:
function a () { console.log(11 } a() //Uncaught SyntaxError: missing ) after argument list Copy the code
The missing close parenthesis in the above code was detected during parsing, throwing a syntax error.
-
Precompilation stage After checking the completion of the grammar analysis stage, now enter the precompilation stage, this stage to prepare for the next stage of function execution, so we first understand the running environment of JS, the running environment is divided into several kinds:
- Global environment (after loading the JS code and passing the grammar analysis stage, enter the global environment)
- Function environment (When a function is called, the function environment is entered, and each function has its own function environment)
- Eval (not recommended, security concerns, not detailed)
In each of these environments, an Execution Context is created for each entry into a different environment, and each individual in the call stack can be understood as an Execution Context. In a call stack, the bottom of the stack is always the global execution context, and the top is the current execution context. So we can understand the execution context and function execution environment as equivalent. When the execution context is created, the following three main events occur:
-
Create VO (Variable Object)
-
Create Scope Chain
-
Make sure this points to
Create the variable object VO
Creating a variable object takes the following steps
- Create arguments objects, check the arguments in the current context, and initialize them, only in the function context (non-arrow functions), not in the global context
- Check the function declarations in the current context, find the function declarations in the current context in code order, and declare them in advance, in the VO of the current context, create an attribute with the function name, the value of the attribute is the heap memory pointer to this function. If there are duplicates, overwrite the pointer to the latest function.
- Check the variable declaration of the current context, and also search in accordance with the code order. If found, in the current VO, establish an attribute named with the variable, and initialize the value as
undefined
If it already exists, the declaration of the variable is ignored.
Note: The window object is the variable object of the global environment. All variables and functions are properties and methods of the window.
So let’s see what VO looks like:
function main(a, b) { var c = 10 function sub() { console.log(c) } } main(1.2) Copy the code
From the above description, we know that this code has a global execution context at the bottom of the call stack. Here we care about the execution context of the main function. We name it mainEC and its structure is as follows
mainEc = { VO: { // A variable object arguments: { // arguments object, which is not necessarily an object, is a class array in the browser a: undefined./ / parameter a b: undefined./ / parameter b length: 2 // The length of the argument is 2 }, sub: <sub reference>, // sub function c: undefined // variable c}, scopeChain: [], // this: window // this pointer to}Copy the code
In this stage, the function has not been executed, but the function has been ready to execute, so the variable Object is not accessible. After entering the execution stage, the variable properties of the variable Object will be assigned, and the variable Object will become Active Object, and these variables can be accessed.
Establish scope chains
Once the VO is established, the next step is to create the scope chain. The scope chain consists of the VO or AO of the current execution context and the AO of the preceding context, all the way to the AO of the global context
var num = 10 function main() { var a = 1 function sub() { var b = 2 return a + b } sub() } main() Copy the code
The execution context of the sub function is abstract
subEC = { VO: { // A variable object b: undefined }, scopeChain: [VO(subEC), AO(main), AO(global)].// Scope chain this: window / / this point } Copy the code
VO(subEC), but some variables are not found in VO(subEC). This will be searched in AO(main) and AO(global). If the variables are found, the function can be executed normally. Can’t find the system will throw a reference error.
Let’s use the above code as an example to understand the concept of a Closure. The above code contains closures
As you can see, chrome browser provides the closure as main.
-
Define a new function inside the function
-
An inner function accesses a variable (or live object) of an outer function
-
The inner function executes, and the outer function is the closure
This points to the
According to the above, we can know that this is specified in the precompilation, in different runtime environment, the direction of this will be different, the specific direction of this can refer to the description of this in MDN, my personal understanding is that this refers to the current environment.
-
Execution phase
The code processed in the execution stack is synchronous, asynchronous code, such as event system, network IO, timer and other functions are provided by who? The answer is the environment, as mentioned before, JS runtime environment provides it with different capabilities, browser environment provides DOM operation ability, DOM event system, etc.; The Node environment provides file system, IO system capabilities that are provided by the API exposed by the JS runtime environment. These apis process some asynchronous tasks, and then return the result (success or failure) to the task queue, waiting for the Event Loop call as shown below:
Now let’s explain the macro JS mechanism in detail.
Runtime environment
From the above figure, we can have a macro understanding of the JS operating mechanism. After the JS engine interprets and executes the code, when the code needs to realize certain functions, such as DOM manipulation and user event monitoring, the operating environment needs to expose some APIS to perform corresponding functions. At present, there are two common operating environments for JS, one is browser environment and the other is Node environment. These two environments provide DIFFERENT functions for JS respectively. For example, Node environment provides JS with the ability to read and write file systems and obtain operating system information. Simply put, JS in different environments can achieve different capabilities and functions.
Event Loop
JS is a single-threaded language, and it is asynchronous. Controlling the asynchronous implementation of JS depends on the famous Event Loop. How to understand the Event Loop? If is sync task is better understood, can be implemented according to the order of writing code, met asynchronous task, but when the real task execution order have no direct relationship with the written order, in the previous article in a general description of the JS asynchronous task execution mechanism, we now from the Event Loop in terms of, How the different asynchrony is scheduled. First of all, from the perspective of macro task and micro task, JS task queue is divided into:
- Microtask queue
- Macro task queue
However, this division is not enough to understand its specific scheduling mechanism, so I will divide it again
-
Microtask queue
NextTickTask Queue
The queue used to hold the process.nexttick () callbackMircoTask Queue
To holdpromise.then/.catch/.finally
Queue for callbacks
-
Macro task queue
-
Global JS code
-
The Timer Queue is used to store the setTimeout and setInterval callbacks. This phase is used to check whether the two timers have reached the specified time. If the time is up, the two timers are sent to the call stack for execution
-
Pengding I/O Callbaks Queue Stores queues for checking and executing callbacks to I/O operations in pending states
This queue queues callbacks to pending I/ OS (such as network I/O, file I/O, etc.). When the I/O completes, the callback is sent to the call stack. The result may be success or failure
-
Idle/prepare Queuelibuv library internal mechanism, we do not care
-
Poll Queue Poll Queue for I/O status
This is the most important stage in the Event Loop and is used to poll for new I/ OS, which are also I/O devices, and if so, to add new tasks to the Pengding I/O Callbaks Queue
-
Check Queue The Queue used to perform setImmediate callbacks
Check that setImmediate does not reach the trigger stage
-
Close Queue Closes the network connection Queue
Perform a callback to a close event, such as the return of the socket’s close
-
The above is the specific execution mechanism of the task queue. In the macro task queue, as long as there are still queues that are not cleared, the Event Loop will continue to Loop until all tasks are cleared. Then after each Loop of the Event Loop, the environment will go back to check whether the microtask queue is empty. If it is empty, the Event Loop will start, but once it is not empty, the system will preferentially execute the tasks of the microtask Queue. We also abstracted two microtask queues before, in which the NextTickTask Queue has a higher priority, so in asynchronous tasks, The callback to Porcess.nexttick () is the first to execute, and it can be interpreted as either executing after the synchronous task completes, or as the first to execute in the asynchronous task.
The above is the JS operation mechanism, there may be some parts of the understanding is not correct or not perfect, later I will slowly supplement.