v8引擎下javascript的工作原理(译文)

175 阅读5分钟

今天,我们将深入了解JavaScript的V8引擎,了解JavaScript是如何执行的。

背景

Web 标准是浏览器规则的集合, 它们定义并描述了 World Wide Web.

W3C是一个为Web开发开放标准的国际团体。他们确保每个人都遵循相同的准则,不必支持几十种完全不同的环境。

现代浏览器是一个代码库执行 成千上万的代码. 因此,它被分成许多模块,负责不同的逻辑。

浏览器的两个最重要的部分是JavaScript执行引擎渲染引擎

Blink 是一个渲染引擎,它负责整个渲染流程,包括DOM树、样式、事件和V8集成。它解析DOM树,解析样式,并确定所有元素的视觉几何结构。

在通过动画帧持续监视动态变化的同时,Blink会在屏幕上绘制内容。JS引擎是浏览器的重要组成部分,但我们还没有了解这些细节。

JavaScript 引擎 101

js引擎将JavaScript编译成机器码并执行. JavaScript引擎执行JavaScript并将其编译为本地机器代码。每个主要浏览器都开发了自己的JS引擎:谷歌的Chrome使用V8,Safari使用JavaScriptCore,Firefox  使用 SpiderMonkey

我们将特别使用V8,因为它在Node.js和Electron中使用,但其他引擎也是以同样的方式构建的。

每一步都将包含一个它的代码的链接,因此您可以熟悉代码库并在本文之外继续研究。

我们将使用a mirror of V8 on GitHub 因为它提供了方便且众所周知的UI来导航代码库。

准备源代码

V8需要做的第一件事是下载源代码。这可以通过network、cache或service workers来完成。

收到代码后,我们需要以编译器能够理解的方式对其进行更改。这个过程称为解析,由两部分组成:扫描器和解析器

扫描器 获取JS文件并将其转换为规定的令牌列表(token list).这里有所有的token列表 the keywords.txt file.

解析器 获取token列表并创建 Abstract Syntax Tree (AST): 一个能代表源代码的树,树的每个节点表示代码的结构.

有个简单的例子:

function foo() {
  let bar = 1;
  return bar;
}

这个代码将会被转为如下结构:

image.png

你可以执行这个代码通过(根、左、右)进行排序遍历:

  1. 定义 foo 函数.
  2. 定义bar 变量.
  3. 1 赋值给 bar.
  4. 返回bar

你可以看到VariableProxy — 一个节点表示内存中的常量. 执行VariableProxy的过程叫做 Scope Analysis.

Just-in-Time (JIT)

通常,为了执行代码,需要将编程语言转换为机器代码。有几种方法可以说明如何以及何时发生这种转变。

转换代码的最常见方法是执行提前编译。它的工作原理与听起来完全一样:在编译阶段执行程序之前,代码被转换为机器代码。

这种方法被许多编程语言使用,如C++、Java等。

在表的另一边,我们有转译(Interpreter):每行代码都将在运行时执行。这种方法通常被JavaScript和Python等动态类型语言采用,因为在执行之前不可能知道确切的类型。

因为提前编译可以一起评估所有代码,所以它可以提供更好的优化,最终生成更高性能的代码。另一方面,解释更容易实现,但通常比编译选项慢。

为了更快、更有效地为动态语言转换代码,创建了一种新方法,称为实时(JIT)编译。它结合了转译和汇编的最佳效果。

当使用解释作为基本方法时,V8可以检测比其他函数更频繁使用的函数,并使用以前执行的类型信息来编译它们。

但是,类型可能会发生变化。我们需要对编译后的代码进行反优化,并回退到转译(Interpreter)(我们可以在获得新的类型反馈后重新编译函数)。

转译Interpreter

V8 使用的转义模型 Ignition. 最初,它采用抽象语法树(ast)来生成字节码。

字节码指令也有元数据。通常,字节码指令与JS抽象相匹配。

现在让我们以上面的示例为例,手动为其生成字节代码:

LdaSmi #1 // 将1写入累加器(accumulator)
Star r0   // 将累加器中的值写入r0 (bar) 
Ldar r0   // 将r0 (bar)的值写入累加器
Return    // 返回累加器的值

(Ignition 的累加器(accumulator) — 一个你能 读取/存储 值的空间.)

蓄能器避免了推动和弹出堆栈顶部的需要。它也是许多字节代码的隐式参数,通常保存操作结果。Return隐式返回累加器。

您可以查看相应源代码中的所有可用字节代码。如果您对其他JS概念(如循环和异步/等待)如何以字节代码表示感兴趣,我觉得阅读这些test expectations很有用。

总结

  1. 始于从网络获取JavaScript代码。

  2. V8解析源代码并将其转换为抽象语法树(AST)。

  3. 基于该AST,Ignition解释器可以开始执行其任务并生成字节码。

  4. 此时,引擎开始运行代码并收集类型反馈。

  5. 为了使其运行得更快,可以将字节码与反馈数据一起发送到优化编译器。优化编译器基于它进行某些假设,然后生成高度优化的机器代码。

  6. 如果在某一点上,其中一个假设被证明是错误的,那么优化编译器将取消优化并返回到解释器。