js核心系列(一) —— v8引擎解析原理

2,100 阅读6分钟

js核心系列流程图.png

最近更新时间:2023年4月24日

讲在前面

所有的JavaScript代码都需要在某种环境中托管和运行。在大多数情况下,该环境是Web浏览器

对于要在Web浏览器中执行的JavaScript代码,很多过程都在幕后进行。在本文中,我们将了解 JavaScript代码在Web浏览器中运行时,幕后所发生的一切。

v8引擎是个啥

V8 是一个由 Google 开发的开源 JavaScript引擎,目前用在 Chrome 浏览器和 Node.js中,其核心功能是执行和解析我们编写的JavaScript代码。

v8解析过程

1678427602178.jpg

上图的V8对于我们来说是一个黑盒,我们首先来详细说下这个黑盒里面的秘密。首先v8引擎接收到js源代码,会有scanner(扫描器),用于对JavaScript代码进行词法分析,它会将代码分析为tokens,然后parser(解析器)会进行一个语法分析的过程,它会将词法分析结果tokens转换为抽象语法树「Abstract Syntax Tree」,同时会验证语法,如果有错误就会抛出语法错误。然后解释器将AST树转换成ByteCode(字节码),然后执行代码。除了解释器,v8引擎还有编译器,比如一个函数执行多次,可能会被识别为热点函数就会经过编译器优化,将Bytecode编译为Optimized Machine Code,以提高代码的执行性能,然后再执行代码。

tokens: 语法上不能再分割的最小单元
AST(抽象语法树): 源代码语法结构的一种抽象表示,以树状的形式表现的语法结构

a238eace-210b-4e8c-8f06-d138e9a65e0c.jpeg

上图中 Ignition是一个解释器,会将AST转换成ByteCode(字节码)

TurboFan是编译器,可以将字节码编译为CPU可以直接执行的机器码,提高代码的执行性能(机器码实际上也会被还原为ByteCode)

再来谈谈预解析

在parse解析代码的过程中,V8并不会一次性将所有的JavaScript代码进行解析,这主要是基于以下两点:

  • 首先,如果一次解析和编译所有的 JavaScript 代码,过多的代码会增加编译时间,这会 严重影响到首次执行 JavaScript 代码的速度,让用户感觉到卡顿。因为有时候一个页面的JavaScript 代码都有 10 多兆,如果要将所有的代码一次性解析编译完成,那么会大大增加用户的等待时间

  • 其次,解析完成的字节码和编译之后的机器代码都会存放在内存中,如果一次性解析和编译所有 JavaScript 代码,那么这些中间代码和机器代码将会一直占用内存,特别是在手机普及的年代,内存是非常宝贵的资源。

所以V8引擎就实现了Lazy Parsing(延迟解析)  的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行

比如当解析顶层代码的时候,遇到了一个函数,但是这个函数只是声明了,并没有调用,那么预解析器就会对该函数做一次快速的预解析,而不是全量解析。

预解析的主要干的事情有两个

第一,是判断当前函数是不是存在一些语法上的错误,如下面这段代码:

function foo(ab) {
   {/}  //语法错误
 }
 var a = 1
 var c = 4
 foo(15)

在预解析过程中,预解析器发现了语法错误,那么就会向 V8 抛出语法错误,比如上面这段代码的语法错误是这样的:

Uncaught SyntaxError: Invalid regular expression: missing /

第二,除了检查语法错误之外,预解析器另外的一个重要的功能就是检查函数内部是否引用了外部变量,如果引用了外部的变量,预解析器会将栈中的变量复制到堆中,在下次执行到该函数的时候,直接使用堆中的引用,这样就解决了闭包所带来的问题。

注意: 预解析,不会生成AST树

帮你加深理解呗

在贴上一张 我很喜欢的图,帮助大家进一步理解v8解析js的过程

b641b60235f806f62f895a5a8130a6d.png

在弄点细节补充

通过上面的图,我们再来补充一些细节:

  • V8 启动执行 JavaScript 之前,它还需要准备执行JavaScript时所需要的一些基础环境,这些基础环境包括了堆空间,栈空间,全局执行上下文,全局作用域,事件循环系统,js内置函数

  • JavaScript 是一种非常灵活的动态语言,对象的结构和属性是可以在运行时任意修改的,而经过优化编译器优化过的代码只能针对某种固定的结构,一旦在执行过程中,对象的结构被动态修改了,那么优化之后的代码势必会变成无效的代码,这时候优化编译器就需要执行反优化操作(如图箭头所示),经过反优化的代码,下次执行时就会回退到解释器解释执行

理解了反优化,有一个建议能够帮助我们避免反优化操作,从而提高代码执行效率。那就是不要总是改变对象类型。

JIT(即时编译)

学到这里我们知道,大体来说,有两种方式可以将程序翻译成机器可执行的指令,使用编译器 (Compiler)  或者是 解释器 (Interpreter)

解释器

解释器是边翻译,边执行。

优缺点:

  • 优点:快速执行,不需要等待编译
  • 缺点:相同的代码可能被翻译多次,比如循环内部的代码

编译器

而编译器则是提前将结果翻译出来,并生成一个可执行程序。

优缺点:

  • 优点:不需要重复编译,并且可以在编译时对代码做优化
  • 缺点:需要提前编译

V8 并没有采用某种单一的技术,而是混合编译执行和解释执行这两种手段,我们把这种混合使用编译器和解释器的技术称为 JIT(Just In Time)技术。

做个总结吧

最后再来总结下,V8 执行一段 JavaScript 代码所经历的主要流程:

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

其他js引擎

js引擎还有其他很多种

本文以v8引擎为例。

参考文章

How JavaScript Works: Under the Hood of the V8 Engine

图解googleV8