preface

Our day-to-day development was done with JavaScript and Web API calls, and we had barely scratched the surface of V8’s understanding of how it worked inside the “black box.”

Understanding this is the only way to write better and more elegant JavaScript code. At the same time, understanding the implementation of JavaScript can also help us understand the syntax and parsing principles of Babel, the syntax checking mechanism of ESLint, and the low-level implementation of the React. Js and Vue front-end frameworks, so as to adapt to new technologies and frameworks.

What is a V8?

V8 is Google’s open source JavaScript engine, which is widely used in a variety of JavaScript execution environments, such as Chrome, Node.js, Electron, and Deno.

How does V8 execute JS code?

When V8 executes JavaScript source code, the parser first parses the source code into an Abstract Syntax Tree, and the Ignotion interprets the AST into bytecode and executes it as it is interpreted.

During this process, the interpreter keeps track of how many times a particular snippet of code has been run, and if the code has been run more than a certain threshold, it is marked as hot code and fed back to the TurboFan.

Based on the feedback, the optimized compiler optimizes and compiles the bytecode to generate optimized machine code, so that when the piece of code is executed again, the interpreter directly uses the optimized machine code to execute without having to interpret it again, greatly improving the efficiency of the code.

This technique of compiling code at run time, also known as JIT (just-in-time compilation), can greatly improve the performance of JavaScript code.

This article starts with V8’s parser.

How does a Parser convert source code to an AST?

To get V8 to execute the source code we’ve written, we need to convert it into a format V8 can understand. V8 begins by Parsing the source code into an abstract syntax tree (AST), an object that represents the tree structure of the source code, a process known as Parsing, which is implemented primarily by V8’s Parser module. The V8 interpreter then compiles the AST into bytecode and executes it as it is interpreted.

The performance of the parsing and compiling process is important because V8 cannot run the code until the compilation is complete (let’s focus on the implementation of the parsing process in V8 for now).

The whole parsing process can be divided into two parts.

  • Tokens are the smallest unit of tokens that cannot be divided in syntax. It can be a single character or a string. Scanner is V8’s lexical analyzer.

  • Parsing: According to the syntax rules, divide tokens into a nested hierarchy of abstract syntax structures called AST. In this process, if the source code does not conform to the syntax specification, parsing will be terminated and syntax errors will be thrown. Both Parser and pre-Parser are V8 parsers.

Lexical analysis

In V8, the Scanner is responsible for receiving a stream of Unicode characters and parsing them as tokens, which are made available to the parser. Var a = 1; Tokens look like this:

[{"type": "Keyword"."value": "var"
    },
        {
        "type": "Identifier"."value": "a"
    },
    {
        "type": "Punctuator"."value": "="
    },
    {
        "type": "Numeric"."value": "1"
    },
    {
        "type": "Punctuator"."value": ";"}]Copy the code

You can see that var a = 1; This line contains five tokens:

  • The keywordvar
  • identifiername
  • The assignment operator=
  • separator;

Syntax analysis

Var a = 1; var A = 1; The JSON structure of the AST generated by this line of code looks like this:

{
  "type": "Program"."start": 0."end": 10."body": [{"type": "VariableDeclaration"."start": 0."end": 10."declarations": [{"type": "VariableDeclarator"."start": 4."end": 9."id": {
            "type": "Identifier"."start": 4."end": 5."name": "a"
          },
          "init": {
            "type": "Literal"."start": 8."end": 9."value": 1."raw": "1"}}]."kind": "var"}]."sourceType": "module"
}

Copy the code

You can see the AST structure of the source code converted by Parser in astexplorer.net/.

However, for a piece of JavaScript source code, if all the source code has to be fully parsed before it can be executed, there are certain problems.

  • Longer code execution time: Parsing all code at once inevitably increases code execution time.
  • Consume more memory: parsedAST, and according toASTThe compiled bytecode is stored in memory, which inevitably takes up more memory.
  • Disk space: Compiled code is cached on disk and occupies disk space.

As a result, mainstream JavaScript engines now implement Lazy Parsing.

Delay resolution

The idea of delayed parsing is simple: During parsing, only pre-parsing (Pre Parser) is done for functions that are not immediately executed, and full parsing is done only when the function is called.

Pre-parser is a pre-parser that verifies function syntax, parses function declarations, determines function scope, and does not generate AST.

function foo(a, b) {
    var res = a + b;
    return res;
}

var a = 1;
var c = 2;
foo(1.2);
Copy the code

Because the Scanner reads code line by line in byte stream, the V8 parser also parses code from top to bottom. When the V8 Parser encounters a function declaration foo, it finds that it is not executed immediately, so it is pre-parsed with the pre-Parser. Only the function declaration is parsed, not the function internal code, and no AST is generated for the function internal code.

The Ignition interpreter then compiles the AST into bytecode and executes it. The interpreter executes the code in top-down order, starting with var a = 1; Var a = 2; Two assignment expressions, and then a function call to foo(1, 2) is executed, at which point the Parser continues parsing the code inside the function, generating the AST, and turning it over to the Ignition interpreter.

conclusion

The above is about V8 when running JavaScript, the parser related knowledge, if not welcome to the big guys correct.

Reference: How does V8 Execute JavaScript Code? Rayken –