V8 是如何执行一段 JavaScript 代码的?

1,287 阅读4分钟

核心流程为两步:

  1. 编译:将 JS 代码转换为低级中间代码或者机器能够理解的机器码
  2. 执行:执行转换后的代码并输出结果

什么是 V8?

V8 是一个 Google 开源的 JS 引擎,可以看成一个虚拟机,通过模拟计算机的各种功能来实现代码的执行。如:模拟实际计算机的 CPU、堆栈、寄存器等,还有 V8 自己的一套指令系统。

高级代码为什么要先编译再执行?

因为 CPU 只能识别二进制指令,但是二进制难以阅读和记忆,于是就有了「汇编指令集」,汇编指令集编写的汇编代码通过汇编编译器可转换成二进制代码,然后交给 CPU 执行。

mov ax,bx         // 汇编指令
1000100111011000  // 机器指令

汇编语言的缺点(为什么会有高级语言?)

  1. 不同的 CPU 有着不同的指令集,如果使用汇编语言来实现一个功能,需要为每种架构的 CPU 编写特定的汇编代码,每出一个新的 CPU 又要重新编写,成本高,收益小。

  2. 编写汇编代码,我们还需要了解和处理器架构相关的硬件知识,比如:使用寄存器、内存、操作 CPU 等。为了让工程师专心处理业务逻辑,就需要一种即能屏蔽计算机架构细节,又能适应多种不同的 CPU 架构的语言。于是就有了高级语言,C、C++、Java、JavaScript 等。

高级语言该如何执行?

CPU 不能直接识别汇编语言,同样也不能识别高级语言,也需要转换成二进制来执行。有下面两种执行方法:

  1. 解释执行
    将输入的源代码通过解析器编译成中间代码,之后直接使用解释器解释执行中间代码,然后输出结果。(解释器通常是软件,不能直接使用硬件,会导致执行效率较低。)

解释执行流程图

  1. 编译执行
    将输入的源代码通过解析器编译成中间代码,之后通过编译器转换成机器代码。机器代码通常以二进制文件的形式直接存储,可以直接执行。

编译执行流程图

V8 是如何执行 JS 代码的?

V8 混合了编译执行和解释执行两种策略。
解释执行启动速度快,但执行速度慢。
编译执行启动速度慢,但执行速度快。

下图为 V8 执行 JS 代码的流程图:

V8 编译流水线

执行 JS 代码的步骤:

  1. 初始化基础环境
    从上图左边部分可以看到,V8 执行 JS 前,需要准备以下基础环境,以便执行过程中使用。
  • 堆、栈空间
  • 全局执行上下文:包含执行过程中全局信息,比如:一些内置函数、全局变量。
  • 全局作用域:包含一些全局变量,在执行过程中的数据需要放在内存中。
  • 事件循环系统:包含消息驱动器和消息队列,不断接受消息并决策如何处理。
  1. 解析源码生成 AST 和作用域
    基础环境准备好后,就要处理 JS 代码,这对 V8 来说只是一堆字符串,不能直接理解,需要将其结构化成抽象语法数(AST)。结构化:指信息经过分析后可以分解成多个互相关联的组成部分,各部分间有明确的层次结构,方便维护和使用。

  2. 依据 AST 和作用域生成字节码并解释执行
    有了 AST 和作用域后,就可以生成字节码了,就是上图中的中间代码。解释器会按顺序执行字节码,并输出结果。

  3. 监听热点代码并优化为二进制机器代码
    旁边的监控机器人是一个监控解释器执行状态的模块,在解释执行字节码的过程中,如果发现一段代码被多次重复执行,就会将其标记为热点代码。V8 会将这段热点代码丢给编译器编译为二进制代码。如果下次再执行时,就会直接执行二进制代码,提高执行速度。

  4. 反优化生成的二进制机器代码
    在 JS 中,对象的结构和属性可以在运行时任意修改,而优化后的代码只针对某种固定结构,一旦结构被修改就会变为无效代码。这时就需要执行反优化操作,下次执行时回退到解释器解释执行。

参考资料

V8 是如何执行一段 JavaScript 代码的?