# JS引擎是什么做了什么
# 为什么需要 JavaScript 引擎
我们写的 JavaScript 代码直接交给浏览器或者 Node 执行时,底层的 CPU 是不认识的,也没法执行。CPU 只认识自己的指令集,指令集对应的是汇编代码。写汇编代码是一件很痛苦的事情。并且不同类型的 CPU 的指令集是不一样的,那就意味着需要给每一种 CPU 重写汇编代码。 JavaScirpt 引擎可以将 JS 代码编译为不同 CPU(Intel, ARM 以及 MIPS 等)对应的汇编代码,这样我们就不需要去翻阅每个 CPU 的指令集手册来编写汇编代码了。当然,JavaScript 引擎的工作也不只是编译代码,它还要负责执行代码、分配内存以及垃圾回收
# 热门JS引擎
- V8 (Google),用 C++编写,开放源代码,由 Google 丹麦开发,是 Google Chrome 的一部分,也用于 Node.js。
- JavaScriptCore (Apple),开放源代码,用于 webkit 型浏览器,如 Safari ,2008 年实现了编译器和字节码解释器,升级为了 SquirrelFish。苹果内部代号为“Nitro”的 JavaScript 引擎也是基于 JavaScriptCore 引擎的。
- Rhino,由 Mozilla 基金会管理,开放源代码,完全以 Java 编写,用于 HTMLUnit
- SpiderMonkey (Mozilla),第一款 JavaScript 引擎,早期用于 Netscape Navigator,现时用于 Mozilla Firefox。
- Chakra (JScript 引擎),用于 Internet Explorer。
# V8引擎介绍
Google V8 引擎是用 C ++编写的开源高性能 JavaScript 和 WebAssembly 引擎,它已被用于 Chrome 和 Node.js 等。可以运行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上。
V8 最早被开发用以嵌入到 Google 的开源浏览器 Chrome 中,第一个版本随着第一版Chrome于 2008 年 9 月 2 日发布。但是 V8 是一个可以独立运行的模块,完全可以嵌入到任何 C ++应用程序中。著名的 Node.js( 一个异步的服务器框架,可以在服务端使用 JavaScript 写出高效的网络服务器 ) 就是基于 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。
和其他 JavaScript 引擎一样,V8 会编译 / 执行 JavaScript 代码,管理内存,负责垃圾回收,与宿主语言的交互等。通过暴露宿主对象 ( 变量,函数等 ) 到 JavaScript,JavaScript 可以访问宿主环境中的对象,并在脚本中完成对宿主对象的操作。
# v8工作流程
# 工作流程概括
(0)初始化基础环境;
(1)词法分析
(2)解析源码生成 AST 和作用域;
(3)依据 AST 和作用域生成字节码;
(4)解释执行字节码;
(5)监听热点代码;
(6)优化热点代码为二进制的机器代码;
(7)反优化生成的二进制机器代码。
# 具体工作流程
词法分析 scanner 是一个扫描器,用于对纯文本的 JavaScript 代码进行词法分析。它会将代码分析为 tokens
语法分析
- parser 模块可以理解为是一个解析器。解析过程是一个语法分析的过程,它会将词法分析结果 tokens 转换为抽象语法树「Abstract Syntax Tree」,同时会验证语法,如果有错误就抛出语法错误。
- 预解析方案:
- 主流的 JavaScript 引擎都采用了惰性解析(Lazy Parsing),因为源码在执行前如果全部完全解析的话,不仅会造成执行时间过长,而且会消耗更多的内存以及磁盘空间。
- 惰性解析就是指如果遇到并不是立即执行的函数,只会对其进行预解析(Pre-Parser),当函数被调用时,才会对其完全解析。
- 预解析时,只会验证函数的语法是否有效、解析函数声明以及确定函数作用域,并不会生成 AST,这项工作由 Pre-Parser 预解析器完成
- 预解析特点:
- 预解析会跳过未被使用的代码
- 不会生成 AST,会产生不带有变量引用和声明的 scopes 信息
- 解析速度快
- 根据规范抛出特定的错误
- 全量解析特点:
- 解析被使用的代码
- 生成 AST
- 构建具体的 scopes 信息,变量的引用,声明等
- 抛出所有的语法错误
此时对应的,其实就是执行上下文的创建过程,关于执行上下文我们后续详细分析。需要区分的是,作用域与作用域链的信息是在预解析阶段就已经明确了。 同时,由于预解析的特点,JavaScript 引擎会重复执行编译和执行这两个阶段,并同时增加和删除执行上下文。所以一个上下文可能会多次被添加删除。
生成字节码
- Ignition 是 v8 提供的一个解释器。
- 负责将 AST 转换为 Bytecode,解释执行 Bytecode;同时收集 TurboFan 优化编译所需的信息,比如函数参数的类型
编译器
TurboFan 是 v8 引擎的编译器模块。它会利用 Ignition 收集到的信息,将字节码转换为优化的机器码,以提高代码的执行性能。
- 如果函数只被调用 1 次,则 Ignition 将其编译 Bytecode 就直接解释执行了。TurboFan 不会进行优化编译,因为它需要 Ignition 收集函数执行时的类型信息。这就要求函数至少需要执行 1 次,TurboFan 才有可能进行优化编译。
- 如果函数被调用多次,则它有可能会被识别为热点函数,且 Ignition 收集的类型信息证明可以进行优化编译的话,这时 TurboFan 则会将 Bytecode 编译为 Optimized Machine Code(已优化的机器码),以提高代码的执行性能(Optimize)。
Ignition + TurboFan 的组合,就是字节码解释器 + JIT 编译器的黄金组合「边解释边执行」。当判断无法优化时就会触发去优化「De-optimize」操作,这些代码逻辑会重新回到 Ignition 中称为字节码。
在这个过程中,有一个建议能够帮助我们避免去优化操作,从而提高代码执行效率。那就是不要总是改变对象类型。