logo学习随笔

【译】JavaScript 可视化之执行引擎

February 03, 2020

作为 JavaScript 开发人员,我们有必要了解关于 JavaScript 引擎的基础,它是如何将 JS 代码编译成机器码的。以下内容会以 V8 引擎为主。


HTML 解析器当遇到 script 标签时,源代码会从该 source 路径从网络缓存或者安装的 service worker 中获取。响应的内容为字节流,将由字节流解码器接管,在下载时就开始进行解码。

byte stream decoder

解码器根据解码的字节生成令牌(tokens)。例如,0066 解码为 f0075 解码为 u006e 解码为 n0063 解码为 c0074 解码为 t0069 解码为 i006f 解码为 o006e 解码为 n 并附带一个空格。就像编写的 function,这是一个保留的关键字,口令生成后会被发送至解析器(还有预解析器,将在后面解释)。其余的字节流也如此解码。

parser

引擎使用两种解析器:预解析器和解析器。为了减少加载网站的时间,引擎尽量避免立即解析非必要的代码。预解析器处理将来可能用到的代码,解析器处理当前用到的代码。如果一个函数只在用户点击按钮后调用,那么在网站加载时就不需要立即去编译这部分代码。如果用户最终按下按钮并请求该段代码,它会被发送至解析器。

解析器基于从字节流解码器接受的令牌来创建节点。通过这些节点来创建抽象语法树(Abstract Syntax Tree)。

ast

下一步是解释器(interpreter)的工作。解释器遍历 AST 并基于 AST 包含的信息生成字节码。一旦字节码完全生成,AST 会被删除,在内存中释放。最终我们得到了机器可运行的代码。

interpreter

尽管机器码很快,但它还可以更快。在机器码运行时,某些信息也会随之生成。引擎可以检测出哪些行为是经常发生的,哪些数据类型被使用。可能有一个函数被调用了很多次,那么引擎就可以对此进行优化使其执行更快。

机器码和生成的类型反馈信息会被发送至优化编译器(optimizing compiler)。该编译器根据这些数据生成高度优化过的机器码。

optimizing compiler

JavaScript 是一种动态类型语言,意味着数据的类型可以不断变化。如果 JavaScript 引擎每次都要检查某个值的数据类型无疑是非常低效的。

为了减少解释代码的时间,优化过的机器码只处理引擎之前遇到的运行的字节码的情况。如果我们重复使用那段代码并返回相同的数据类型,优化过的机器码可以直接重复使用来加快运行。然而,当同一段代码返回了不同类型的数据,机器码未优化,引擎只能回退到解释生成的字节码的阶段。

例如下面的 sum 函数,它每次调用接收数字类型的参数:

function sum(a, b) {
  return a + b;
}

sum(1, 2);

结果返回了 3,下次我们调用,它会假设我们仍然传入两个数字的值。

如果是这样的,不需要动态检索,引擎可以直接使用优化过的机器码。否则,如果假设是错误的,它会回退到最初的字节码而不是优化过的机器码。

例如,下次我们调用时传入了一个字符串参数:

function sum(a, b) {
  return a + b;
}

sum('1', 2);

这会使数字 2 强制转换成字符串,函数最终会返回字符串 "12"。引擎会执行解释过的字节码并更新类型反馈。

相关推荐

V8 文档 | V8 Github | Chrome University 2018: Life Of A Script

参考

https://dev.to/lydiahallie/javascript-visualized-the-javascript-engine-4cdf