JS引擎执行一段js的过程

最初,JavaScript 只能在 Web 浏览器中运行,但是随着 Node 的出现,现在 JavaScript 也可以在服务端运行。虽然我们可能知道应该在何时何地去使用它, 但是我们真的了解这些脚本执行的背后发生了什么吗?

JavaScript 是一门高级语言,但是最终计算机能理解只有1和0。那么我们编写的代码是如何被计算机理解的呢?掌握所学编程语言的基础知识将让您能编写出更好的代码。在本文中,我们仅探讨一个问题:JS引擎是如何工作的?

一、JavaScript 引擎

这是本文将要探索的主要内容,它负责使计算机理解我们编写的 JS 代码。JavaScript 引擎是一种用于将我们的代码转换为机器可读语言的引擎。如果没有 JavaScript 引擎,您编写的代码对计算机来说简直是一堆“胡言乱语”。不仅仅是 JavaScript ,其他所有编程语言都需要一个类似的引擎,来将这些“胡言乱语”转换成对计算机有意义的语言。

接下来,我们将深入研究这些引擎,以了解它们是如何翻译 JavaScript 文件的。

二、JavaScript 引擎的内里

JavaScript 引擎只是分析我们的代码并将其翻译成另一种语言的工具。V8 是最受欢迎的 JavaScript 引擎之一,也是 Chrome 和 NodeJS 使用的引擎。它是用 C++(一种底层语言)编写的。但是如果每个人都创造一个引擎,那场面就不是可控范围内的了。

因此,为了给这些引擎确立一个规范,ECMA 的标准诞生了,该标准主要提供如何编写引擎和 JavaScript 所有功能的规范。这就是新功能能在 ECMAScript 6、7、8 上实现的原因。同时,引擎也进行了更新以支持这些新功能。于是,我们便可以在开发过程中检查了浏览器中 JS 高级功能的可用性。

下面我们对 V8 引擎进行进一步的探索,因为基本概念在所有引擎中是一致的。

JavaScript V8 Engine

 

上图就是 JS Engine 内部的工作流程。我们输入的代码将通过以下阶段,

  • Parser

  • AST

  • Interpreter 生成 ByteCode

  • Profiler

  • Compiler 生成优化后的代码

别被上面的流程给唬住了,在几分钟后您将了解它们是协同运作的。

在进一步深入这些阶段之前,您需要先了解 Interpreter 和 Compiler 的区别。

三、Interpreter VS Compiler

通常,将代码转换成机器可读语言的方法有两种。我们将要讨论的概念不仅适用于 JavaScript ,而且适用于大多数编程语言,例如 Python,Java 等。

  • Interpreter 逐行读取代码并立即执行。

  • Compiler 读取您的整个代码,进行一些优化,然后生成优化后的代码。

让我们来看下面这个例子。

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

for (let i = 0; i < 1000; i++) {
  add(1 + 1);
}

上面的示例循环调用了 add 函数1000次,该函数将两个数字相加并返回总和。

Interpreter 接收上面的代码后,它将逐行读取并立即执行代码,直到循环结束。它的工作仅仅是实时地将代码转换为我们的计算机可以理解的内容。

如果这段代码接受者是 Compiler,它会先完整地读取整个程序,对我们要执行的代码进行分析,并生成电脑可以读懂的机器语言。过程如同获取 X(我们的JS文件)并生成 Y(机器语言)一样。如果我们使用 Interpreter 执行 Y,则会获得与执行 X 相同的结果。

从上图中可以看出,ByteCode 只是中间码,计算机仍需要对其进行翻译才能执行。但是 Interpreter 和 Compiler 都将源代码转换为机器语言,它们唯一的区别在于转换的过程不尽相同。

  • Interpreter 逐行将源代码转换为等效的机器代码。

  • Compiler 在一开始就将所有源代码转换为机器代码。

如果你想了解更多它们之前的区别,推荐阅读这篇文章。

当您阅读完上面的推荐文章后,您可能已经了解到 Babel 实际上是一个 JS Compiler ,它可以接收您编写的新版本 JS 代码并向下编译为与浏览器兼容的 JS 代码(旧版本的 JS 代码)。

Interpreter 和 Compiler 的优缺点

Interpreter 的优点是无需等待编译即可立即执行代码。这对在浏览器中运行 JS 提供了极大的便利,因为所有用户都不想浪费时间在等待代码编译这件事上。但是,当有大量的 JS 代码需要执行时会运行地比较慢。还记得上面例子中的那一小段代码吗?代码中执行了1000次函数调用。函数 add 被调用了1000次,但他的输出保持不变。但是 Interpreter 还是逐行执行,会显得比较慢。

在同样的情况下,Compiler 可以通过用2代替循环(因为 add 函数每次都是执行1 + 1)来进行一些优化。Compiler 最终给出的优化代码可以在更短的时间内执行完成。

综上所述,Interpreter 可以立即开始执行代码,但不会进行优化。Compiler 虽然需要花费一些时间来编译代码,但是会生成对执行时更优的代码。

好的,Interpreter 和 Compiler 必要知识我们已经了解了。现在让我们回到主题——JS 引擎。

因此,考虑到编译器和解释器的优缺点,如果我们同时利用两者的优点,该怎么办?这就是 JIT(Just In Time) Compiler 的用武之地。它是 Interpreter 和 Compiler 的结合,现在大多数浏览器都在更快,更高效地实现此功能。同时 V8 引擎也使用此功能(如第一张图)。

在这个过程中,Parser 是一种通过各种 JavaScript 关键字来识别,分析和分类程序各个部分的解析器。它可以区分代码是一个方法还是一个变量。

然后,AST(抽象语法树) 基于 Parser 的分类构造树状结构。您可以使用 AST Explorer 查看该树的结构。

随后将 AST 提供给 Interpreter 生成 ByteCode。如上文所述,ByteCode 不是最底层的代码,但可以被执行。在此阶段,浏览器借助 V8 引擎执行 ByteCode 进行工作,因此用户无需等待。

同时,Profiler 将查找可以被优化的代码,然后将它们传递给 Compiler。Compiler 生成优化代码的同时,浏览器暂时用 ByteCode 执行操作。并且,一旦 Compiler 生成了优化代码,优化代码则将完全替换掉临时的 ByteCode。

通过这种方式,我们可以充分利用 Interpreter 和 Compiler 的优点。Interpreter 执行代码的同时,Profiler 寻找可以被优化的代码,Compiler 则创建优化的代码。然后,将 ByteCode 码替换为优化后的较为底层的代码,例如机器代码。

这仅意味着性能将在逐渐提高,同时不会有阻塞执行的时间。

关于 ByteCode

作为机器代码,ByteCode 不能被所有计算机理解及执行。它仍然需要像虚拟机或像 Javascript V8 引擎这样的中间件才能将其转换为机器可读的语言。这就是为什么我们的浏览器可以在上述5个阶段中借助 JavaScript 引擎在 Interpreter 中执行 ByteCode 的原因。

所以您可以会有另一个问题,

JavaScript 是一门解释型语言吗?

JavaScript 是但不完全是一门解释型语言。Brendan Eich 最初是在 JavaScript 的早期阶段创建 JavaScript 引擎 “ SpiderMonkey” 的。该引擎有一个 Interpreter 来告诉浏览器该怎么执行代码。但是现在我们的引擎不仅包括了 Interpreter,还有 Compiler。我们的代码不仅可以被转换成 ByteCode,还可以被编译输出优化后的代码。因此,从技术上讲,这完全取决于引擎是如何实现的。

JavaScript 引擎的整体工作原理就是这样。相信您无需学习 JavaScript 也可以理解。当然,您甚至可以在不知道 JavaScript 如何工作的情况下编写代码。但是,如果我们了解一些幕后的知识,或许能让我们编写出更好的代码。

四、V8 执行一段JS代码的过程

站在 V8 的角度,理解其中的执行机制,能够帮助我们理解很多的上层应用,包括Babel、Eslint、前端框架的底层机制。那么,一段 JavaScript 代码放在 V8 当中究竟是如何执行的呢?

JS属于解释型语言,对于解释型的语言说,解释器会对源代码做如下分析:

  • 通过词法分析和语法分析生成 AST(抽象语法树)
  • 生成字节码

然后解释器根据字节码来执行程序。但 JS 整个执行的过程其实会比这个更加复杂,接下来就来一一地拆解。

1.生成 AST

生成 AST 分为两步——词法分析和语法分析。

词法分析即分词,它的工作就是将一行行的代码分解成一个个token。 比如下面一行代码:

let name = 'sanyuan'

其中会把句子分解成四个部分:

 

即解析成了四个token,这就是词法分析的作用。

接下来语法分析阶段,将生成的这些 token 数据,根据一定的语法规则转化为AST。举个例子:

let name = 'sanyuan'
console.log(name)

最后生成的 AST 是这样的:

 

当生成了 AST 之后,编译器/解释器后续的工作都要依靠 AST 而不是源代码。顺便补充一句,babel 的工作原理就是将 ES6 的代码解析生成ES6的AST,然后将 ES6 的 AST 转换为 ES5 的AST,最后才将 ES5 的 AST 转化为具体的 ES5 代码。由于本文着重阐述原理,关于 babel 编译的细节就不展开了,推荐大家去读一读荒山的babel文章, 帮你打开新世界的大门: )

回到 V8 本身,生成 AST 后,接下来会生成执行上下文

2. 生成字节码

开头就已经提到过了,生成 AST 之后,直接通过 V8 的解释器(也叫Ignition)来生成字节码。但是字节码并不能让机器直接运行,那你可能就会说了,不能执行还转成字节码干嘛,直接把 AST 转换成机器码不就得了,让机器直接执行。确实,在 V8 的早期是这么做的,但后来因为机器码的体积太大,引发了严重的内存占用问题。

给一张对比图让大家直观地感受以下三者代码量的差异:

 

 

很容易得出,字节码是比机器码轻量得多的代码。那 V8 为什么要使用字节码,字节码到底是个什么东西?

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

字节码仍然需要转换为机器码,但和原来不同的是,现在不用一次性将全部的字节码都转换成机器码,而是通过解释器来逐行执行字节码,省去了生成二进制文件的操作,这样就大大降低了内存的压力。

3. 执行代码

接下来,就进入到字节码解释执行的阶段啦!

在执行字节码的过程中,如果发现某一部分代码重复出现,那么 V8 将它记做热点代码(HotSpot),然后将这些代码编译成机器码保存起来,这个用来编译的工具就是V8的编译器(也叫做TurboFan) , 因此在这样的机制下,代码执行的时间越久,那么执行效率会越来越高,因为有越来越多的字节码被标记为热点代码,遇到它们时直接执行相应的机器码,不用再次将转换为机器码。

其实当你听到有人说 JS 就是一门解释器语言的时候,其实这个说法是有问题的。因为字节码不仅配合了解释器,而且还和编译器打交道,所以 JS 并不是完全的解释型语言。而编译器和解释器的 根本区别在于前者会编译生成二进制文件但后者不会。

并且,这种字节码跟编译器和解释器结合的技术,我们称之为即时编译(JIT)

这就是 V8 中执行一段JS代码的整个过程,梳理一下:

  1. 首先通过词法分析和语法分析生成 AST
  2. 将 AST 转换为字节码
  3. 由解释器逐行执行字节码,遇到热点代码启动编译器进行编译,生成对应的机器码, 以优化执行效率

五、同类型文章

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章