JavaScript 是如何工做的系列——第二篇
在上一篇《JavaScript 从下载到执行(阻塞、defer、async)》中介绍了浏览器是什么时候开始下载和执行 JavaScript 的 ,以及阻塞 HTML 解析问题。本篇文章将深刻 JavaScript 引擎,了解 JavaScript 引擎(V8)是如何执行 JavaScript 代码的。javascript
咱们都知道 JavaScript 代码是不能直接在 CPU 中运行的,须要将 JavaScript 代码翻译成机器能够识别的指令(二进制码)。那么谁来翻译呢?没错,就是 JavaScript 引擎。固然 JavaScript 引擎并非简单的将 JavaScript 代码翻译成 CPU 可执行的机器代码,这中间还涉及了不少优化策略。html
JavaScript 引擎是一个负责整个 JavaScript 程序执行的应用程序,它是浏览器引擎的一部分。如今也被应用于 Node.js 中。前端
大多数主流的浏览器对于 JavaScript 引擎都有本身的实现:java
本篇文章将以目前最为流行的 Chrome 浏览器的 V8 引擎为例,进行介绍。git
下图展现了 V8 引擎工做的基本流程:github
其中 Parser(解析器)、Ignition(解释器)、TurboFan(编译器) 是 V8 中三个主要的工做模块,除此以外还有一个主要的工做模块是 Orinoco(垃圾回收)。编程
工做流程概述:segmentfault
下面咱们将详细介绍这四个步骤。数组
V8 引擎首先会扫描全部的源代码,进行词法分析(词法分析是经过 Scanner 模块来完成的,本文不进行详细介绍)。浏览器
什么是词法分析?
词法分析(Tokenizing/Lexing)就是将程序源代码分解成对编程语言来讲有意义的代码块,这些代码块被称为词法单元(token)。
咱们来看下 var a = 2;
这句代码通过词法分析后会被分解出哪些 tokens ?
从上图中能够看到,这句代码最终被分解出了五个词法单元:
var
关键字a
标识符=
运算符2
数值 ;
分号Tokens 在线查看网站: https://esprima.org/demo/pars...
Parser 是 V8 的解析器,负责根据生成的 Tokens 进行语法分析。Parser 的主要工做包括:
词法做用域相关内容,敬请期待文章《JavaScript 之做用域》
咱们简单介绍下什么是抽象语法树(Abstract Syntax Tree, AST)?
仍是上面的例子,咱们来看下 var a = 2;
通过语法分析后生成的AST是什么样子的:
能够看到这段程序的类型是 VariableDeclaration,也就是说这段代码是用来声明变量的。
AST 在线查看网站: https://astexplorer.net/
什么是预解析 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 。
咱们仍是以示例来讲明:
function foo() { console.log('a'); function inline() { console.log(''b) } } (function bar() { console.log('c') })(); foo();
(function bar() {console.log(c)})()
时,它会知道这是一个当即执行表达式(IIFE),会当即被执行,因此会使用 Parser 对其解析。Ignition 是 V8 的解释器,它负责的工做包括:
什么是字节码?
字节码(Bytecode)是一种中间码,它比机器码更抽象,也更轻量,须要直译器转译后才能成为机器码的中间代码。
早期版本的 V8 ,并无生成中间字节码的过程,而是将全部源码转换为了机器代码。机器代码虽然执行速度更快,可是占用内存大。
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 是 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