V8是什么?
V8 是一个由 Google 开发的开源 JavaScript 引擎,目前用在 Chrome 浏览器和 Node.js 中,其核心功能是执行易于人类理解的 JavaScript 代码。
调试安装
npm install --global jsvu
输入jsvu,会出现选择系统版本,选择系统版本后,会出现选在安装的javascript引擎,选择安装v8,v8-debug
选择系统版本
输入jsvu,会出现选择系统版本
选择安装javascript引擎
选择v8, v8-debug
添加.jsvu添加到系统环境变量
vi ~/.zshrc
source ~/.zshrc
V8如何执行javascript代码
核心: 编译 + 执行。编译是将javascript代码编译成机器可以识别的语言,然后执行转换后的代码,输出对应的结果。
如何理解V8:我们可以把V8当成一个虚拟机,通过模拟实际计算机的各种功能来实现代码的执行,如模拟实际计算机的 CPU、堆栈、寄存器等,虚拟机还具有它自己的一套指令系统。
**如何理解代码执行:**计算机执行高级语言的两种方式
解释执行:需要先将输入的源代码通过解析器编译成中间代码,之后直接使用解释器解释执行中间代码,然后直接输出结果。Python/JavaScript
编译执行:需要通过解析器先将源代码转换为中间代码,然后我们的编译器再将中间代码编译成机器代码。通常编译成的机器代码是以二进制文件形式存储的,需要执行这段程序的时候直接执行二进制文件就可以了。还可以使用虚拟机将编译后的机器代码保存在内存中,然后直接执行内存中的二进制代码,C,C++
两者区别:解释型需要将程序解释成机器码来运行,并没有保存机器码,是在运行过程中进行,
而编译型在运行之前就已经让编译器给程序编译成机器码了。
那么V8是解释执行还是编译执行呢?
答案:V8是混合编译执行和解释执行这两种手段,我们把这种混合使用编译器和解释器的技术称为 JIT(Just In Time)技术。结合了两者的优点:解释执行的启动速度快,但是执行时的速度慢,而编译执行的启动速度慢,但是执行时的速度快。
V8执行javascript代码流程
-
启动V8环境:在V8环境执行javascript代码之前就准备好了所需要的基础环境。
- 栈空间用来管理javascript调用,编译代码时候入栈,执行时出栈
- 堆空间存储对象类型的离散的数据。javascript除原生数据类型外,其他的对象类型,诸如函数,数组,浏览器的 window 对象,document 对象等,都是存储在堆空间
- 事件循环系统:V8寄生在宿主环境中的,本身没有自己的主线程,而是使用宿主环境提供的主线程,V8 自身,与 V8 执行的代码,都运行在宿主环境的主线程。主线程需要一个消息队列,存放 V8 内部的,页面响应的,JavaScript 触发的各种任务;还需要一个事件循环,不断地从消息队列中取出任务来执行。
-
编译解析:
- 词法解析:用于将代码拆分为 “最小的,不可分割的单位”,它被叫做 “token”。比如关键字
for,if,和一些直接量,如123这样的数字。 - 语法解析:用于将已经拆分后的 token ,生成抽象语法树(AST)。之后,再根据 AST 生成执行上下文和作用域。
- 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 1d. 全局作用域: 在全局作用域中 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] } - 词法解析:用于将代码拆分为 “最小的,不可分割的单位”,它被叫做 “token”。比如关键字
-
解释器生成字节码
// 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) -
监视器:如果这段代码被重复执行,监控器就会将其标记为热点代码,并提交给编译器优化执行。
// 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 -
反优化:JavaScript 是一门动态语言,在运行过程中,某些被优化的结构可能会被 V8 动态修改了,这会导致之前被优化的代码失效,如果某块优化之后的代码失效了,那么编译器需要执行反优化操作。
总结:v8环境执行javascript过程
初始化基础环境 ⇒解析源码生成 AST 和作用域 ⇒依据 AST 和作用域生成字节码⇒解释执行字节码⇒监听热点代码⇒优化热点代码为二进制的机器代码⇒反优化生成的二进制机器代码。
通过V8是理解函数变量提升
- 函数声明: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
}
}
-
函数表达式
全局变量中有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 并不会将表达式中的函数对象提升到全局作用域中,所以无法在函数表达式之前使用该函数。