您的位置:首页 >聚焦 >

浅析V8引擎,让你更懂JavaScript!

2022-03-19 06:02:29    来源:程序员客栈

导语|本文介绍了编译、解释、动静态语言等基本概念,以及V8引擎的基本流程。本文将对其进行详细阐述,希望为更多的开发者提供经验和帮助。

一、编译与解释

二进制指令就是机器码:

编译:将源代码一次性转换成目标代码的过程。执行编译过程的程序叫编译器(Compiler)。

解释:将源代码逐条转换成目标代码,同时逐条运行的过程。执行解释过程的程序叫解释器(Interpreter)。解释器一般来说就是vm,vm有两种,一种是基于堆栈,一种是基于寄存器。

编译过程大致包括词法分析、语法分析、语义分析、性能优化、生成可执行文件等五个步骤,期间涉及到复杂的算法和硬件架构。解释器与此类似。

二、静态语言与动态语言

高级语言按照执行方式的不同,可分为静态语言和动态语言。

静态语言:使用编译执行的语言,如C、C++、Golang等。使用编译器一次性生成目标代码,“一次编译,无限次运行”,程序运行速度更快。编译型语言一般是不能跨平台的,也就是不能在不同的操作系统之间随意切换。

动态语言:使用解释执行的语言,如Python、Javascript、PHP等。执行过程中需要源代码,只要存在解释器,源代码可以在任何操作系统上运行,可移植性好,“一次编写,到处运行”。

解释型语言之所以能够跨平台,是因为有了解释器这个中间层。在不同的平台下,解释器会将相同的源代码转换成不同的机器码,解释器帮助我们屏蔽了不同平台之间的差异。

java和C#是一种比较奇葩的存在,它们是半编译半解释型的语言,源代码需要先转换成一种中间文件(字节码文件),然后再将中间文件拿到虚拟机中执行。Java引领了这种风潮,它的初衷是在跨平台的同时兼顾执行效率;C#是后来的跟随者,但是C#一直止步于Windows平台,在其它平台鲜有作为。

总结:

三、V8引擎

Javascript是解释型语言,那么V8引擎就对应着解释器。但是V8引擎为了提高JS的运行效率,会提前编译。

也就是V8引擎包括两个阶段:编译、执行,编译阶段指V8将JavaScript转换为字节码或者二进制机器码,执行阶段指解释器解释执行字节码,或者CPU直接执行二进制机器码。

(一)JIT

V8引擎同时采用了解释执行和编译执行这两种方式,也就是在运行时进行编译,这种方式称为JIT (Just in Time) 即时编译。

V8在执行JavaScript源码时,会先通过解析器将源码解析成AST,解释器会将AST转化为字节码,一边解释一遍执行。

解释器同时会记录某一代码片段的执行次数,如果执行次数超过了某个阈值,这段代码便会被标记为热代码(Hot Code),同时将运行信息反馈给优化编译器TurboFan,TurboFan根据反馈信息,会优化并编译字节码,最后生成优化的机器码。

(二)Parser生成抽象语法树

Parser生成AST抽象语法树过程包括语法分析、词法分析,和Babel等工具差不多。

生成AST中的一个优化是惰性解析(Lazy Parsing),因为源码在执行前如果全部完全解析的话,不仅执行时间过长,而且会消耗更多的内存。

惰性解析就是指如果遇到并不是立即执行的函数,只会对其进行预解析(Pre-Parser),当函数被调用时,才会对其完全解析。

预解析时,只会验证函数的语法是否有效、解析函数声明以及确定函数作用域,并不会生成AST,这项工作由Pre-Parser预解析器完成。

(三)Ignition生成字节码

字节码是机器码的抽象,可以看作是小型的构建块。相比机器码,字节码不仅占用内存少,而且生成字节码的时间很快,提升了启动速度。

另外,字节码与特定类型的机器码无关,通过解释器将字节码转换为机器码后才可以执行,这样也使得V8更加方便的移植到不同的CPU架构。

可以通过如下命令,查看JavaScript代码生成的字节码。

node --print-bytecode index.js

注意,解释器执行字节码前,还是会将字节码转为机器码,因为计算机只识别机器码。

(四)TurboFan

Ignition执行上一步生成的字节码,并记录代码运行的次数等信息,如果同一段代码执行了很多次,就会被标记为 “HotSpot”(热点代码),然后把这段代码发送给 编译器TurboFan。

然后TurboFan把它编译为更高效的机器码储存起来,等到下次再执行到这段代码时,就会用现在的机器码替换原来的字节码进行执行,这样大大提升了代码的执行效率。

另外,当TurboFan判断一段代码不再为热点代码的时候,会执行去优化的过程,把优化的机器码丢掉,然后执行过程回到Ignition。

TurboFan做的优化包括内联(inlining)和逃逸分析(Escape Analysis)。

内联就是将相关联的函数进行合并,减少运行时间。比如:

function add(a, b) {return a + b}function foo() {return add(2, 4)}

内联处理后:

function fooAddInlined() {var a = 2var b = 4var addReturnValue = a + breturn addReturnValue}// 因为 fooAddInlined 中 a 和 b 的值都是确定的,所以可以进一步优化function fooAddInlined() {return 6}

逃逸分析就是分析对象的生命周期是否仅限于当前函数,如果是的话会对其进行优化。比如:

function add(a, b){const obj = { x: a, y: b }return obj.x + obj.y}

会处理成:

function add(a, b){const obj_x = aconst obj_y = breturn obj_x + obj_y}

四、总体流程

参考资料:

1.v8

2.编译型语言和解释型语言的区别

3.编译器与解释器的区别

4.js引擎能做到多小

5.深入理解JS引擎

6.V8是如何执行JavaScript代码的

7.JIT为什么能大幅度提升性能

8.JIT(just-in-time)即时编译

作者简介

杨国旺

腾讯前端开发工程师

腾讯前端开发工程师,欢迎讨论前端问题。

关键词: 目标代码 解释型语言 解释执行

相关阅读