V8引擎简介——如何编译和执行JS代码
1.用处
- chrome浏览器的引擎
- Nodejs的运行时环境
- electron的底层引擎
2.什么是V8引擎
常见的浏览器一般由七个模块组成,User Interface(用户界面)、Browser engine(浏览器引擎)、Rendering engine(渲染引擎)、Networking(网络)、JavaScript Interpreter(js解释器)、UI Backend(UI 后端)、Date Persistence(数据持久化存储)
提到Chrome浏览器,一般人会认为使用的Webkit内核,这种说法不完全准确。Chrome发布于2008年,使用的渲染内核是Chromium,它是fork自Webkit,但把Webkit梳理得更有条理可读性更高,效率提升明显。2013年,由于Webkit2和Chromium在沙箱设计上的冲突,谷歌联手Opera自研和发布了Blink内核,逐步脱离了Webkit的影响。所以,可以这么认为:Chromium扩展自Webkit止于Webkit2,其后Chrome切换到了Blink内核。另外,Chrome的JS引擎使用的V8引擎,应该算是最著名和优秀的开源JS引擎,大名鼎鼎的Node.js就是选用V8作为底层架构。
而Blink引擎和V8引擎又是什么关系的?Blink 是 Google Chrome 浏览器的渲染引擎,V8 是Google Chrome 浏览器的 JavaScript 引擎。
- 是用C++编写的Google开源高性能JS和WebAssembly引擎
- 简而言之:是一个接收JS代码,编译代码然后执行的C++程序,编译后的代码可以在多种操作系统,多种处理器上运行
主要的工作:
- 编译和执行JS代码
- 处理调用栈
- 内存的分配
- 垃圾的回收
3.溯源
大部分JS引擎在编译和执行JS代码都会用到三个重要的组件:
-
解析器
- 负责将JS源代码解析成抽象语法树AST(通过词法分析、语法分析形成AST),生成执行上下文
- JS反编译,语法解析;
- Babel编译 ES6语法;
- 代码高亮;
- 关键字匹配;
- 代码压缩。
- 负责将JS源代码解析成抽象语法树AST(通过词法分析、语法分析形成AST),生成执行上下文
-
解释器
- 负责将AST解释成字节码bytecode
- 也有直接解释执行bytecode的能力
-
编译器
- 负责编译出运行更加高效的机器代码
早期的V8引擎是如何编译和执行JS代码的:
在V8早期5.9版本之前,V8引擎没有解释器,却有两个编译器:
-
JS由解析器解析后,生成AST抽象语法树
-
然后由编译器(Full-codegen)直接使用AST来编译出机器代码,而不进行任何中间转换
Full-codegen又称基准编译器,因为它生成的是一个基准的未被优化的机器代码
-
还有另一个编译器(Crankshaft)——优化编译器:用来优化代码,提升性能
缺陷:
- 生成的机器码会占用大量的内存
- 缺少中间的字节码,很多性能优化策略无法实施——V8性能提升缓慢
- 无法很好的支持和优化JS的新语法特性
4.目前的V8引擎
- 网页初始化解析执行JS的时间缩短了,网页能够更快的onload
- 在生成的优化机器代码时,不需要从源码重新编译,而使用字节码,并且当需要回退字节码时,只需要回归到中间层的字节码解释执行就可以了
5.V8引擎中处理JS过程中的一些优化策略
如果一次性全部解析出来所有代码,会占用磁盘空间,也非常消耗内存,并且执行的时间会很长。 所以存在预解析,比如当词法分析阶段读到函数声明时,因为函数不需要立即执行,所以会进行预解析。「即只解析函数声明,不解析函数内部的代码,不会为函数生成AST」。 Pre-Parser 就是用来预解析的。当函数调用的时候才会真正开始解析生成AST。最终会将AST转成字节码。
-
如果函数只是声明而未被调用,则不会被解析生成AST
-
函数如果只被调用一次,bytecode直接被解释执行
-
函数被调用多次,可能会被标记为热点函数,可能会被编译成机器代码——提高代码的执行性能。
-
之后执行这个函数时,就直接运行优化后的机器代码
-
随着JS代码的不断执行,会有更多的代码被标记为热点代码,也就会产生更多的机器代码
-
此时会有一个问题:回退字节码。
-
造成的原因是:如一个sum函数,参数是a,b,多次调用传入整数,且被识别为热点函数,解释器将收集到参数和函数信息编译成优化后的机器码,这里就会假定了sum函数的参数就是整形的
-
但是如果某次你传入的不是整数,而是字符串,机器不知道如何处理字符串类型的参数,此时就会deoptimization(回退字节码)
-
总结:不要把一个变量类型变来变去,传入的参数的类型也要固定一下
-
-
解释器
解释器 Ignition 除了负责生成字节码之外,它还有另外一个作用,就是解释执行字节码。通常,如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。在 Ignition 执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
补充:Ignition->点火器,TurboFan->螺旋增压,寓意着代码启动时通过点火器慢慢发动,一旦启动,涡轮增压介入,其执行效率随着执行时间越来越高。
执行上下文
行上下文是对 JavaScript 代码执行环境的一种抽象,每当 JavaScript 运行时,它都是在执行上下文中运行。
执行上下文创建阶段会做三件事:
- 绑定
this - 创建词法环境
- 创建变量环境
-
全局执行上下文 —— 当 JS 引擎执行全局代码的时候,会编译全局代码并创建执行上下文,它会做两件事:1、创建一个全局的 window 对象(浏览器环境下),2、将 this 的值设置为该全局对象;全局上下文在整个页面生命周期有效,并且只有一份。
-
函数执行上下文 —— 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
-
eval 执行上下文 —— 调用 eval 函数也会创建自己的执行上下文(eval函数容易导致恶意攻击,并且运行代码的速度比相应的替代方法慢,因此不推荐使用)
词法作用域
JavaScript采用词法作用域(lexical scoping),也就是静态作用域。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
执行foo函数,首先从 foo 函数内部查找是否有变量 value ,如果没有
就根据书写的位置,查找上面一层的代码,我们发现value等于1,所以结果会打印 1。
// 例1:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
// 例2:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();