V8初探

353 阅读7分钟

V8是什么?

V8 是一个由 Google 开发的开源 JavaScript 引擎,目前用在 Chrome 浏览器和 Node.js 中,其核心功能是执行易于人类理解的 JavaScript 代码。

Untitled.png

调试安装

npm install --global jsvu

输入jsvu,会出现选择系统版本,选择系统版本后,会出现选在安装的javascript引擎,选择安装v8,v8-debug

选择系统版本

输入jsvu,会出现选择系统版本

Untitled.png

选择安装javascript引擎

选择v8, v8-debug

Untitled.png

添加.jsvu添加到系统环境变量

vi ~/.zshrc
source ~/.zshrc

Untitled.png

V8如何执行javascript代码

核心: 编译 + 执行。编译是将javascript代码编译成机器可以识别的语言,然后执行转换后的代码,输出对应的结果。

Untitled.png

如何理解V8:我们可以把V8当成一个虚拟机,通过模拟实际计算机的各种功能来实现代码的执行,如模拟实际计算机的 CPU、堆栈、寄存器等,虚拟机还具有它自己的一套指令系统。

**如何理解代码执行:**计算机执行高级语言的两种方式

解释执行:需要先将输入的源代码通过解析器编译成中间代码,之后直接使用解释器解释执行中间代码,然后直接输出结果。Python/JavaScript

Untitled.png

编译执行:需要通过解析器先将源代码转换为中间代码,然后我们的编译器再将中间代码编译成机器代码。通常编译成的机器代码是以二进制文件形式存储的,需要执行这段程序的时候直接执行二进制文件就可以了。还可以使用虚拟机将编译后的机器代码保存在内存中,然后直接执行内存中的二进制代码,C,C++

Untitled.png

两者区别:解释型需要将程序解释成机器码来运行,并没有保存机器码,是在运行过程中进行,

而编译型在运行之前就已经让编译器给程序编译成机器码了。

那么V8是解释执行还是编译执行呢?

答案:V8是混合编译执行和解释执行这两种手段,我们把这种混合使用编译器和解释器的技术称为 JIT(Just In Time)技术。结合了两者的优点:解释执行的启动速度快,但是执行时的速度慢,而编译执行的启动速度慢,但是执行时的速度快。

V8执行javascript代码流程

Untitled.png

  1. 启动V8环境:在V8环境执行javascript代码之前就准备好了所需要的基础环境。

    1. 栈空间用来管理javascript调用,编译代码时候入栈,执行时出栈
    2. 堆空间存储对象类型的离散的数据。javascript除原生数据类型外,其他的对象类型,诸如函数,数组,浏览器的 window 对象,document 对象等,都是存储在堆空间
    3. 事件循环系统:V8寄生在宿主环境中的,本身没有自己的主线程,而是使用宿主环境提供的主线程,V8 自身,与 V8 执行的代码,都运行在宿主环境的主线程。主线程需要一个消息队列,存放 V8 内部的,页面响应的,JavaScript 触发的各种任务;还需要一个事件循环,不断地从消息队列中取出任务来执行。
  2. 编译解析:

    1. 词法解析:用于将代码拆分为 “最小的,不可分割的单位”,它被叫做 “token”。比如关键字 forif,和一些直接量,如 123 这样的数字。
    2. 语法解析:用于将已经拆分后的 token ,生成抽象语法树(AST)。之后,再根据 AST 生成执行上下文和作用域。
    3. AST结构:
    // test.js
    var a = 1;
    // 执行命令
    v8-debug --print-ast test.js
    // 结果
    [generating bytecode for function: ]
    --- AST ---
    FUNC at 0
    . KIND 0
    . LITERAL ID 0
    . SUSPEND COUNT 0
    . NAME ""
    . INFERRED NAME ""
    . DECLS
    . . VARIABLE (0x7f7a16020850) (mode = VAR, assigned = true) "a"
    . BLOCK NOCOMPLETIONS at -1
    . . EXPRESSION STATEMENT at 282
    . . . INIT at 282
    . . . . VAR PROXY unallocated (0x7f7a16020850) (mode = VAR, assigned = true) "a"
    . . . . LITERAL 1
    

    d. 全局作用域: 在全局作用域中 a = 1并没有执行,此时a = undefined

    // test.js
    var a = 1;
    // 执行命令
    v8-debug --print-scopes test.js
    // 结果
    Global scope:
    global { // (0x7fe378023a30) (0, 284)
      // will be compiled
      // 1 stack slots
      // 3 heap slots
      // temporary vars:
      TEMPORARY .result;  // (0x7fe378023d40) local[0]
      // local vars:
      LET a;  // (0x7fe378023c50) context[2]
    }
    
  3. 解释器生成字节码

    // test.js
    var a = 1;
    // 执行命令
    v8-debug --print-bytecode test.js
    // 结果
    [generated bytecode for function:  (0x1c85081d3265 <SharedFunctionInfo>)]
    Bytecode length: 6
    Parameter count 1
    Register count 1
    Frame size 8
    OSR nesting level: 0
    Bytecode Age: 0
             0x1c85081d32d2 @    0 : 0d 01             LdaSmi [1]
             0x1c85081d32d4 @    2 : 25 02             StaCurrentContextSlot [2]
             0x1c85081d32d6 @    4 : 0e                LdaUndefined 
             0x1c85081d32d7 @    5 : a9                Return 
    Constant pool (size = 0)
    Handler Table (size = 0)
    Source Position Table (size = 0)
    
  4. 监视器:如果这段代码被重复执行,监控器就会将其标记为热点代码,并提交给编译器优化执行。

    // test.js 
    for (let i = 0; i < 10; i++) {
        console.log(10);
    }
    // 执行命令
    v8-debug --trace-opt test.js
    // 结果
    10
    10
    10
    10
    10
    10
    10
    10
    10
    10
    
  5. 反优化:JavaScript 是一门动态语言,在运行过程中,某些被优化的结构可能会被 V8 动态修改了,这会导致之前被优化的代码失效,如果某块优化之后的代码失效了,那么编译器需要执行反优化操作。

总结:v8环境执行javascript过程

初始化基础环境 ⇒解析源码生成 AST 和作用域 ⇒依据 AST 和作用域生成字节码⇒解释执行字节码⇒监听热点代码⇒优化热点代码为二进制的机器代码⇒反优化生成的二进制机器代码。

通过V8是理解函数变量提升

  1. 函数声明:V8 会在内存中为声明生成函数对象,并将该对象提升到作用域中
// fn.js
function add(x, y) {
  console.log(x + y);
}
// 执行命令
v8-debug --print-scopes fn.js
// 结果
Inner function scope: // 函数作用域
function add () { // (0x7fa261023620) (287, 319)
  // 2 heap slots
  // local vars:
  VAR x;  // (0x7fa261019648) never assigned
  VAR y;  // (0x7fa261019690) never assigned
}
Global scope: // 全局作用域
global { // (0x7fa261023430) (0, 334)
  // will be compiled
  // local vars:
  VAR add;  // (0x7fa2610237e0) 

  function add () { // (0x7fa261023620) (287, 319) 对整个函数对象提升
    // lazily parsed
    // 2 heap slots
  }
}
  1. 函数表达式

    全局变量中有fn,匿名函数,但是由于表达式是不会在编译阶段执行的。故fn为undefined

$ v8-debug --print-scopes fn2.js
Inner function scope:
function () { // (0x7fdd5501d880) (290, 317)
  // 2 heap slots
  // local vars:
  VAR x;  // (0x7fdd52817048) never assigned
  VAR y;  // (0x7fdd52817090) never assigned
}
Global scope:
global { // (0x7fdd5501d630) (0, 317)
  // will be compiled
  // 1 stack slots
  // temporary vars:
  TEMPORARY .result;  // (0x7fdd5501db00) local[0]
  // local vars:
  VAR fn;  // (0x7fdd5501d850) 

  function () { // (0x7fdd5501d880) (290, 317)
    // lazily parsed
    // 2 heap slots
  }
}

总结:

函数声明,V8 在编译阶段,都会对其执行变量提升的操作,将它们提升到作用域中,在执行阶段,如果使用了某个变量,就可以直接去作用域中去查找。不过 V8 对于提升函数和提升变量的策略是不同的,如果提升了一个变量,那么 V8 在将变量提升到作用域中时,还会为其设置默认值 undefined,如果是函数声明,那么 V8 会在内存中创建该函数对象,并提升整个函数对象。

函数表达式也是表达式的一种,在编译阶段,V8 并不会将表达式中的函数对象提升到全局作用域中,所以无法在函数表达式之前使用该函数。