JS引擎是如何在幕后工作的

时间:2022-08-23 23:57:02

介绍

您是否曾经问过自己“这在幕后如何运作?”。我知道我有

在接下来的系列文章中,我们将深入探讨JS的世界,从引擎到幕后工作原理,从引擎到提升,执行上下文,词法环境等概念。

对某些概念有深刻的了解可以更好地理解代码,在我们的工作中表现更好,此外,它在面试中非常有用。

而且这可能是一个非常有趣的学科……

在开始之前,还有一件重要的事情要提到-每个JS引擎的构建方式都是不同的,因此无法涵盖它们的全部工作原理。因此,我们将探索V8的工作原理,但是其他引擎中的概念仍然非常相似,只是其中一些引擎可能会实现不同的功能。

以下是V8工作原理的概述:

JS引擎是如何在幕后工作的

如果您还不了解这些内容,请不要担心,在本文结尾处,您将了解该图的每个步骤。

因此,顺便去吧!

环境

计算机,编译器甚至浏览器实际上无法“理解”用JS编写的代码。如果是这样,代码如何运行?

在后台,JS始终在特定环境中运行,最常见的是:

  • 浏览器(迄今为止最常见)
  • Node.js(这是一个运行时环境,允许您在浏览器外部(通常在服务器中)运行JS)

引擎

JS引擎是如何在幕后工作的

因此,JS需要在特定环境中运行,但是该环境到底是什么?

当您使用JS编写代码时,会以人类可读的语法(包括字母和数字)来编写代码。如前所述,机器无法理解此类代码。

这就是每个环境都有引擎的原因。

通常,引擎的工作是获取该代码并将其转换为用机器代码编写的代码,该代码最终可以由计算机处理器运行。

每个环境都有自己的引擎,最常见的引擎是Chrome V8(Node也使用该引擎),Firefox SpiderMonkey,Safari的JavaScriptCore和IE的Chakra。

所有引擎的工作方式都相似,但是每个引擎之间存在差异。

同样重要的是要记住,在后台引擎只是一个软件,例如Chrome V8是用C ++编写的软件。

解析器

JS引擎是如何在幕后工作的

因此,我们有一个环境,并且在该环境中有一个引擎。引擎在执行代码时要做的第一件事是使用解析器检查代码。

解析器了解JS语法和规则,它的工作是逐行检查代码,并检查代码的语法是否正确。

如果解析器遇到错误,它将停止运行并发出错误。如果代码有效,则解析器会生成称为“抽象语法树”(简称AST)的内容

抽象语法树(AST)

JS引擎是如何在幕后工作的

因此,我们的环境中有一个引擎,其中包含一个解析器,该解析器生成AST。但是什么是AST,为什么我们需要它?

AST是一种数据结构,它不是JS所独有的,而是由许多其他语言的编译器实际使用的(其中一些语言是Java,C#,Ruby,Python)。

AST只是代码的树形表示,引擎创建AST而不是直接编译为机器代码的主要原因是,当您将代码包含在树数据结构中时,转换为机器代码更容易。

实际上,您可以查看AST的外观,只需将任何代码放入ASTExplorer网站中,并查看创建的数据结构:

JS引擎是如何在幕后工作的

解释器

JS引擎是如何在幕后工作的

解释器的工作是获取已创建的AST,并将其转换为代码的中间表示(IR)。

我们将在需要进一步上下文以充分了解其含义的基础上,尽快了解有关解释器的更多信息。

中级代表制(IR)

那么,解释器从AST生成的IR是什么?

IR是代表源代码的数据结构或代码。它的作用是介于以JS之类的抽象语言编写的代码与机器代码之间的中间步骤。

本质上,您可以将IR视为机器代码的抽象。

IR的类型很多,在JS引擎中非常流行的是字节码。这是一张图片,展示了IR在V8引擎中的作用:

JS引擎是如何在幕后工作的

但是您可能会问……为什么我们需要IR?为什么不直接编译为机器代码呢?引擎将IR用作高级代码和机器代码之间的中间步骤的主要原因有两个:

  • 为Intel处理器编写的机器代码和为ARM处理器编写的机器代码将有所不同。另一方面,IR可以像通用的那样匹配两者,并且可以匹配任何平台。这使得下面的转换过程更加容易和移动。
  • 优化-与IR相比,使用IR进行优化更容易,从代码优化和硬件优化的角度来看都是如此。

有趣的事实:JS引擎并不是唯一使用字节码作为IR的引擎,在也使用字节码的语言中,您会发现C#,Ruby,Java等。

编译器

JS引擎是如何在幕后工作的

编译器的工作是获取解释器创建的IR(在我们的示例中为Bytecode),然后通过某些优化将其转换为机器代码。

让我们谈谈代码编译和一些基本概念。请记住,这是一个巨大的主题,需要花费大量时间来掌握,因此在我们的使用案例中,我将只对它进行一般性的介绍。

解释器vs编译器

有两种方法可以使用编译器和解释器将代码转换为机器可以运行的机器语言。

解释器和编译器之间的区别在于,解释器翻译您的代码并逐行执行,而编译器在执行之前将所有代码立即翻译成机器代码。

每种方法各有利弊,编译器启动很快,但是复杂且启动缓慢,解释器虽然简单但速度较慢。

话虽如此,有3种方法可以将高级代码转换为机器代码并运行它:

  • 解释-使用这种策略,您会有一个解释器,它逐行执行代码并执行(效率不高)。
  • 提前进行时间编译(AOT)-在这里,您需要一个编译器来首先编译整个代码,然后才执行它。
  • 即时编译— JOT编译策略结合了AOT策略和解释策略,试图从两个方面吸取最大的优势,执行动态编译,还允许进行某些优化,这实际上加快了编译过程。我们将解释有关JIT编译的更多信息。

大多数JS引擎使用JIT编译器,但不是全部。例如,React Native使用的引擎Hermes,没有使用JIT编译器。

综上所述,编译器采用由解释器创建的IR并从中生成优化的机器代码。

JIT编译器

就像我们说的那样,大多数JS引擎都使用JIT编译方法。JIT结合了AOT策略和解释,允许进行某些优化。让我们更深入地研究这些优化以及编译器的功能。

JIT编译优化是通过重复执行代码并对其进行优化来完成的。优化过程如下:

本质上,JIT编译器通过收集执行代码的概要分析数据来获得反馈,如果它遇到任何热代码段(重复自身的代码),则该热段将通过编译器,然后编译器将使用此信息重新更优化地编译。

假设您有一个函数,该函数返回对象的属性:

function load(obj) {  

  return obj.x; 

看起来简单吗?也许对我们来说,但是对于编译器而言,这并不是一件容易的事。如果编译器看到一个对象,它不知道任何东西,则它必须检查属性x的位置,如果该对象确实具有这样的属性,它在内存中的位置,在原型链中的位置等等。

那么如何优化它呢?

为了理解这一点,我们必须知道在机器代码中,对象及其类型已保存。

假设我们有一个具有x和y属性的对象,x是数字类型,而y是字符串类型。从理论上讲,该对象将用如下的机器代码表示:

如果我们调用具有相同对象结构的函数,则可以完成优化。这意味着属性将相同且顺序相同,但值可以不同,如下所示:

load({x: 1, y: 'hello'}); 

load({x: 5, y: 'world'}); 

load({x: 3, y: 'foo'}); 

load({x: 9, y: 'bar'}); 

运作方式如下。调用该函数后,优化的编译器将识别出我们正在尝试调用已被再次调用的函数。

然后,它将继续检查作为参数传递的对象是否具有相同的属性。

如果是这样,它将已经能够访问其在内存中的位置,而不用浏览原型链并完成对未知对象所做的许多其他事情。

本质上,编译器通过优化和反优化过程运行。

当我们运行代码时,编译器假定函数将使用与以前使用的类型相同的类型,因此它将代码与类型预先保存在一起。这种类型的代码称为优化机器代码。

每次代码再次调用相同的函数时,优化的编译器将尝试访问内存中的相同位置。

但是由于JS是一种动态类型的语言,因此在某些时候,我们可能希望将相同的函数用于不同的类型。在这种情况下,编译器将执行去优化过程,并正常地编译代码。

总结一下有关JIT编译器的部分,JIT编译器的工作是通过使用热代码段来提高性能,当编译器执行之前执行的代码时,它假定类型相同并且使用已生成的优化代码,但是如果类型不同,则JIT会执行去优化并正常地编译代码。

关于性能的说明

改善应用程序性能的一种方法是对不同的对象使用相同的类型。如果您具有相同类型的两个不同对象,即使值不同,只要属性具有相同的顺序并具有相同的类型,则编译器会将这两个对象视为具有相同结构和类型的对象,并且可以更快地访问它。

例如:

const obj = { x: 1, a: true, b: 'hey' }  

const obj2 = { x: 7, a: false, b: 'hello' } 

从示例中可以看到,我们有两个具有不同值的不同对象,但是由于属性的顺序和类型相同,因此编译器将能够更快地编译这些对象。

但是,即使有可能以这种方式优化代码,但我认为对于性能而言,还有许多重要的事情要做,而这无关紧要的事情。

在团队中执行这样的事情也很困难,并且由于引擎非常快,因此总体上看并没有太大的区别。

话虽如此,我已经看到V8团队成员推荐了此技巧,所以也许您确实希望有时尝试遵循它。在可能的情况下遵循它不会对我造成任何伤害,但绝对不会以干净的代码和体系结构决策为代价

概要

JS引擎是如何在幕后工作的

JS代码必须在环境中运行,最常见的是浏览器和Node.js。

该环境需要有一个引擎,该引擎需要采用以人类可读的语法编写的JS代码,然后将其转换为机器代码。

引擎使用解析器逐行浏览代码,并检查语法是否正确。如果有任何错误,代码将停止执行并引发错误。

如果所有检查都通过,则解析器将创建一个称为抽象语法树(AST)的树数据结构。

AST是一种数据结构,以树状结构表示代码。通过AST将代码转换为机器代码更加容易。

然后,解释器继续进行AST并将其转换为IR,这是机器代码的抽象,并且是JS代码和机器代码之间的中介。IR还可以执行优化,并且移动性更强。

然后,JIT编译器通过编译代码,获取动态反馈并使用该反馈改进编译过程,从而将生成的IR转换为机器代码。

原文链接:https://medium.com/coralogix-engineering/how-js-works-behind-the-scenes-the-engine-9f15bba95a15) 

文章来源:https://www.toutiao.com/i6923913817919488515/