JavaScript 是如何工作的系列——第二篇
前言
在上一篇《JavaScript 从下载到执行(阻塞、defer、async)》中介绍了浏览器是何时开始下载和执行 JavaScript 的 ,以及阻塞 HTML 解析问题。本篇文章将深入 JavaScript 引擎,了解 JavaScript 引擎(V8)是如何执行 JavaScript 代码的。
什么是 JavaScript 引擎
我们都知道 JavaScript 代码是不能直接在 CPU 中运行的,需要将 JavaScript 代码翻译成机器可以识别的指令(二进制码)。那么谁来翻译呢?没错,就是 JavaScript 引擎。当然 JavaScript 引擎并不是简单的将 JavaScript 代码翻译成 CPU 可执行的机器代码,这中间还涉及了很多优化策略。
JavaScript 引擎是一个负责整个 JavaScript 程序执行的应用程序,它是浏览器引擎的一部分。现在也被应用于 Node.js 中。
大多数主流的浏览器对于 JavaScript 引擎都有自己的实现:
本篇文章将以目前最为流行的 Chrome 浏览器的 V8 引擎为例,进行介绍。
V8 引擎
下图展示了 V8 引擎工作的基本流程:
其中 Parser(解析器)、Ignition(解释器)、TurboFan(编译器) 是 V8 中三个主要的工作模块,除此之外还有一个主要的工作模块是 Orinoco(垃圾回收)。
工作流程概述:
- 首先 V8 引擎会扫描所有的源代码,进行词法分析,生成 Tokens;
- Parser 解析器根据 Tokens 生成 AST;
- Ignition 解释器将 AST 转换为字节码,并解释执行;
- TurboFan 编译器负责将热点函数优化编译为机器指令执行;
下面我们将详细介绍这四个步骤。
词法分析
V8 引擎首先会扫描所有的源代码,进行词法分析(词法分析是通过 Scanner 模块来完成的,本文不进行详细介绍)。
什么是词法分析?
词法分析(Tokenizing/Lexing)就是将程序源代码分解成对编程语言来说有意义的代码块,这些代码块被称为词法单元(token)。
我们来看下 var a = 2;
这句代码经过词法分析后会被分解出哪些 tokens ?
从上图中可以看到,这句代码最终被分解出了五个词法单元:
-
var
关键字 -
a
标识符 -
=
运算符 -
2
数值 -
;
分号
Tokens 在线查看网站:https://esprima.org/demo/pars...
语法分析
Parser
Parser 是 V8 的解析器,负责根据生成的 Tokens 进行语法分析。Parser 的主要工作包括:
- 分析语法错误:遇到错误的语法会抛出异常;
- 输出 AST:将词法分析输出的词法单元流(数组)转换为一个由元素逐级嵌套所组成的代表了程序语法结构的树——抽象语法树(Abstract Syntax Tree, AST);
- 确定词法作用域;
词法作用域相关内容,敬请期待文章《JavaScript 之作用域》
我们简单介绍下什么是抽象语法树(Abstract Syntax Tree, AST)?
还是上面的例子,我们来看下 var a = 2;
经过语法分析后生成的AST是什么样子的:
可以看到这段程序的类型是 VariableDeclaration,也就是说这段代码是用来声明变量的。
AST 在线查看网站:https://astexplorer.net/
Pre-Parser
什么是预解析 Pre-Parser?
我们先来看看下面这段代码:
function foo () {
console.log('I\'m function foo')
}
function bar () {
console.log('I\'m function bar')
}
foo()
上面这段代码中,如果使用 Parser 解析后,会生成 foo 函数 和 bar 函数的 AST。然而 bar 函数并没有被调用,所以生成 bar 函数的 AST 实际上是没有任何意义且浪费时间的。那么有没有办法解决呢?此时就用到了 Pre-Parser 技术。
在 V8 中有两个解析器用于解析 JavaScript 代码,分别是 Parser 和 Pre-Parser 。
- Parser 解析器又称为 full parser(全量解析) 或者 eager parser(饥饿解析)。它会解析所有立即执行的代码,包括语法检查,生成 AST,以及确定词法作用域。
- Pre-Parser 又称为惰性解析,它只解析未被立即执行的代码(如函数),不生成 AST ,只确定作用域,以此来提高性能。当预解析后的代码开始执行时,才进行 Parser 解析。
我们还是以示例来说明:
function foo() {
console.log('a');
function inline() {
console.log(''b)
}
}
(function bar() {
console.log('c')
})();
foo();
- 当 V8 引擎遇到 foo 函数声明时,发现它未被立即执行,就会采用 Pre-Parser 对其进行解析(inline 函数同)。
- 当 V8 遇到
(function bar() {console.log(c)})()
时,它会知道这是一个立即执行表达式(IIFE),会立即被执行,所以会使用 Parser 对其解析。 - 当 foo 函数被调用时,会使用 Parser 对 foo 函数进行解析,此时会对 inline 函数再进行一次预解析,也就是说 inline 函数被预解析了两次。如果嵌套层级较深,那么内层的函数会被预解析多次,所以在写代码时,尽可能避免嵌套多层函数,会影响性能。
Ignition
Ignition 是 V8 的解释器,它负责的工作包括:
- 将 AST 转换为中间代码(字节码 Bytecode)
- 逐行解释执行字节码:在该阶段,就已经开始执行 JavaScript 代码了。
什么是字节码?
字节码(Bytecode)是一种中间码,它比机器码更抽象,也更轻量,需要直译器转译后才能成为机器码的中间代码。
早期版本的 V8 ,并没有生成中间字节码的过程,而是将所有源码转换为了机器代码。机器代码虽然执行速度更快,但是占用内存大。
TurboFan
TurboFan 是 V8 的优化编译器,负责将字节码和一些分析数据作为输入并生成优化的机器代码。
上面我们说到,当 Ignition 将 JavaScript 代码转换为字节码后,程序就可以执行了,那么 TurboFan 还有什么用呢?
我们再来看下 V8 的工作流程图:
我们主要关注 Ignition 和 TurboFan 的交互:
当 Ignition 开始执行 JavaScript 代码后,V8 会一直观察 JavaScript 代码的执行情况,并记录执行信息,如每个函数的执行次数、每次调用函数时,传递的参数类型等。
如果一个函数被调用的次数超过了内设的阈值,监视器就会将当前函数标记为热点函数(Hot Function),并将该函数的字节码以及执行的相关信息发送给 TurboFan。TurboFan 会根据执行信息做出一些进一步优化此代码的假设,在假设的基础上将字节码编译为优化的机器代码。如果假设成立,那么当下一次调用该函数时,就会执行优化编译后的机器代码,以提高代码的执行性能。
那如果假设不成立呢?不知道你们有没有注意到上图中有一条由 optimized code 指向 bytecode 的红色指向线。此过程叫做 deoptimize(优化回退),将优化编译后的机器代码还原为字节码。
读到这里,你可能有些疑惑:这个假设是什么假设呢?以及为什么要优化回退?我们来看下面的例子。
function sum (a, b) {
return a + b;
}
我们都知道 JavaScript 是基于动态类型的,a 和 b 可以是任意类型数据,当执行 sum 函数时,Ignition 解释器会检查 a 和 b 的数据类型,并相应地执行加法或者连接字符串的操作。
如果 sum 函数被调用多次,每次执行时都要检查参数的数据类型是很浪费时间的。此时 TurboFan 就出场了。它会分析监视器收集的信息,如果以前每次调用 sum 函数时传递的参数类型都是数字,那么 TurboFan 就预设 sum 的参数类型是数字类型,然后将其编译为机器指令。
但是当某一次的调用传入的参数不再是数字时,表示 TurboFan 的假设是错误的,此时优化编译生成的机器代码就不能再使用了,于是就需要进行优化回退。
Orinoco
Orinoco 是 V8 的垃圾回收模块(garbage collector),负责将程序不再需要的内存空间回收;
下一篇
本篇文章主要介绍了 JavaScript 引擎(V8)执行 JavaScript 代码的工作流程,主要涉及了Parser、Ignition、TurboFan、 Orinoco 四个模块。下篇文章将开始讲解 JavaScript 执行机制中的核心概念——执行上下文。
参考:
The Journey of JavaScript: from Downloading Scripts to Execution - Part
The Journey of JavaScript: from Downloading Scripts to Execution - Part II
视野前端(二)V8引擎是如何工作的
What is V8?
JavaScript 引擎 V8 执行流程概述
Ignition: An Interpreter for V8 [BlinkOn]
How JavaScript works: an overview of the engine, the runtime, and the call stack
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。