0%

V8 如何执行 js 代码

编译器和解释器

编译语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次程序运行时,都可以直接运行该二进制文件,而不需要重新编译了。例如C/CppGo

解释性语言的代码则每次在运行的时候都需要通过解释器对程序进行动态解释和执行。例如 JSPython

两者流程如下图:

image-20200608233116104

  1. 编译型语言编译过程中,编译器会依次对源码进行词法分析、语法分析,生成 AST,然后优化代码,最后再生成处理器能够理解的机器码。编译成功会生成一个二进制文件。
  2. 解释型语言解释过程中,解释器同样会对源代码进行同样的 AST 生成操作,但最后会生成字节码,然后根据字节码来执行程序、输出结果。

V8 如何执行一段 js 代码

具体流程图为:

image-20200609091652173

图中可以看到,v8在执行过程中即有解释器,也有编译器。

1. 生成 AST 和 执行上下文

将源代码转换成 AST,并生成执行上下文(代码执行过程中的环境信息)。

AST 的结构和代码结构很相似,编译器和解释器后续的工作都依赖于 AST,而不是源代码。

AST 是非常重要的一种数据结构,很多项目中中广泛应用。Babel 也是利用 AST 搞出来的。它的工作原理就是先将 ES6 源码转换成 AST,然后再将 ES6 源码的 AST 转换为 ES5 的 AST,最后利用 ES5 的 AST 来生成 js 源代码。

Eslint 也是,它在检查过程中会将源码转换为 AST,然后再利用 AST 来检查代码规范化的问题。

AST的生成步骤是:

第一阶段是分词,又称为词法分析,其作用是将一行行源代码拆解成一个个 token(语法上不可能再分的,最小的单个字符或字符串)。

image-20200609092620922

上图就是四个 token。

第二阶段是解析(parse),又称为语法分析,作用是将 token 根据语法规则转换成 AST。

有了 AST 之后,V8 就会生成执行上下文。

2. 生成字节码

有了 AST 和 context,下一步就是解释器根据 AST 来生成字节码,并解释执行字节码。

其实 v8 一开始是直接将 AST 转换为执行效率高的机器码,但之后在手机引入 Chrome 之后,就出现了内存占用的问题,于是 v8 团队又重新引入了字节码。

字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。

对比一下高级代码、字节码和机器码,可以参考下图:

image-20200609093356436

图中可以看出,机器码占用内存远大于字节码。

3. 执行代码

生成字节码之后,就进入执行阶段。

通常第一次执行的字节码会被解释器逐条解释执行。如果有多次执行的字节码(热点代码),后台的编译器会把这段热点代码的字节码编译成高效的机器码,当再次执行这段被优化的代码时,只用执行编译后的机器码就可以了,这样大大提高了代码的执行效率。

其实字节码配合解释器和编译器是很火的一种技术,例如 Python 和 Java 的虚拟机也是基于这种技术,我们把这种技术称为即时编译(JIT)

这么多语言和工作引擎都使用了“字节码 + JIT”技术,其具体工作流程如下:

image-20200609094654544

js 性能优化

随着 v8 架构的完善,我们越来越不需要考虑一些例如隐藏类、内联缓存策略的微优化策略了。因此相较于优化 js 的执行效率,我们应该将优化的中心放在单次脚本的执行时间和脚本的网络下载上面:

  • 提高单次脚本的执行速度,避免 js 的长任务霸占主线程,这样可以使页面快速响应交互。
  • 避免大的内联脚本,因为在解析 HTML 的过程中,解析和编译也会占用主线程。
  • 减少 js 文件的容量,因为更小的文件会提升下载速度,并且占用更低的内存。