今天,我们将深入了解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;
}
这个代码将会被转为如下结构:
你可以执行这个代码通过(根、左、右)进行排序遍历:
- 定义
foo函数. - 定义
bar变量. - 将
1赋值给bar. - 返回
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很有用。
总结
-
始于从网络获取JavaScript代码。
-
V8解析源代码并将其转换为抽象语法树(AST)。
-
基于该AST,Ignition解释器可以开始执行其任务并生成字节码。
-
此时,引擎开始运行代码并收集类型反馈。
-
为了使其运行得更快,可以将字节码与反馈数据一起发送到优化编译器。优化编译器基于它进行某些假设,然后生成高度优化的机器代码。
-
如果在某一点上,其中一个假设被证明是错误的,那么优化编译器将取消优化并返回到解释器。