JS引擎是如何在幕后工作的
介紹
您是否曾經(jīng)問過自己“這在幕后如何運作?”。我知道我有
在接下來的系列文章中,我們將深入探討JS的世界,從引擎到幕后工作原理,從引擎到提升,執(zhí)行上下文,詞法環(huán)境等概念。
對某些概念有深刻的了解可以更好地理解代碼,在我們的工作中表現(xiàn)更好,此外,它在面試中非常有用。
而且這可能是一個非常有趣的學(xué)科……
在開始之前,還有一件重要的事情要提到-每個JS引擎的構(gòu)建方式都是不同的,因此無法涵蓋它們的全部工作原理。因此,我們將探索V8的工作原理,但是其他引擎中的概念仍然非常相似,只是其中一些引擎可能會實現(xiàn)不同的功能。
以下是V8工作原理的概述:
如果您還不了解這些內(nèi)容,請不要擔(dān)心,在本文結(jié)尾處,您將了解該圖的每個步驟。
因此,順便去吧!
環(huán)境
計算機,編譯器甚至瀏覽器實際上無法“理解”用JS編寫的代碼。如果是這樣,代碼如何運行?
在后臺,JS始終在特定環(huán)境中運行,最常見的是:
- 瀏覽器(迄今為止最常見)
- Node.js(這是一個運行時環(huán)境,允許您在瀏覽器外部(通常在服務(wù)器中)運行JS)
引擎
因此,JS需要在特定環(huán)境中運行,但是該環(huán)境到底是什么?
當(dāng)您使用JS編寫代碼時,會以人類可讀的語法(包括字母和數(shù)字)來編寫代碼。如前所述,機器無法理解此類代碼。
這就是每個環(huán)境都有引擎的原因。
通常,引擎的工作是獲取該代碼并將其轉(zhuǎn)換為用機器代碼編寫的代碼,該代碼最終可以由計算機處理器運行。
每個環(huán)境都有自己的引擎,最常見的引擎是Chrome V8(Node也使用該引擎),F(xiàn)irefox SpiderMonkey,Safari的JavaScriptCore和IE的Chakra。
所有引擎的工作方式都相似,但是每個引擎之間存在差異。
同樣重要的是要記住,在后臺引擎只是一個軟件,例如Chrome V8是用C ++編寫的軟件。
解析器
因此,我們有一個環(huán)境,并且在該環(huán)境中有一個引擎。引擎在執(zhí)行代碼時要做的第一件事是使用解析器檢查代碼。
解析器了解JS語法和規(guī)則,它的工作是逐行檢查代碼,并檢查代碼的語法是否正確。
如果解析器遇到錯誤,它將停止運行并發(fā)出錯誤。如果代碼有效,則解析器會生成稱為“抽象語法樹”(簡稱AST)的內(nèi)容
抽象語法樹(AST)
因此,我們的環(huán)境中有一個引擎,其中包含一個解析器,該解析器生成AST。但是什么是AST,為什么我們需要它?
AST是一種數(shù)據(jù)結(jié)構(gòu),它不是JS所獨有的,而是由許多其他語言的編譯器實際使用的(其中一些語言是Java,C#,Ruby,Python)。
AST只是代碼的樹形表示,引擎創(chuàng)建AST而不是直接編譯為機器代碼的主要原因是,當(dāng)您將代碼包含在樹數(shù)據(jù)結(jié)構(gòu)中時,轉(zhuǎn)換為機器代碼更容易。
實際上,您可以查看AST的外觀,只需將任何代碼放入ASTExplorer網(wǎng)站中,并查看創(chuàng)建的數(shù)據(jù)結(jié)構(gòu):
解釋器
解釋器的工作是獲取已創(chuàng)建的AST,并將其轉(zhuǎn)換為代碼的中間表示(IR)。
我們將在需要進一步上下文以充分了解其含義的基礎(chǔ)上,盡快了解有關(guān)解釋器的更多信息。
中級代表制(IR)
那么,解釋器從AST生成的IR是什么?
IR是代表源代碼的數(shù)據(jù)結(jié)構(gòu)或代碼。它的作用是介于以JS之類的抽象語言編寫的代碼與機器代碼之間的中間步驟。
本質(zhì)上,您可以將IR視為機器代碼的抽象。
IR的類型很多,在JS引擎中非常流行的是字節(jié)碼。這是一張圖片,展示了IR在V8引擎中的作用:
但是您可能會問……為什么我們需要IR?為什么不直接編譯為機器代碼呢?引擎將IR用作高級代碼和機器代碼之間的中間步驟的主要原因有兩個:
- 為Intel處理器編寫的機器代碼和為ARM處理器編寫的機器代碼將有所不同。另一方面,IR可以像通用的那樣匹配兩者,并且可以匹配任何平臺。這使得下面的轉(zhuǎn)換過程更加容易和移動。
- 優(yōu)化-與IR相比,使用IR進行優(yōu)化更容易,從代碼優(yōu)化和硬件優(yōu)化的角度來看都是如此。
有趣的事實:JS引擎并不是唯一使用字節(jié)碼作為IR的引擎,在也使用字節(jié)碼的語言中,您會發(fā)現(xiàn)C#,Ruby,Java等。
編譯器
編譯器的工作是獲取解釋器創(chuàng)建的IR(在我們的示例中為Bytecode),然后通過某些優(yōu)化將其轉(zhuǎn)換為機器代碼。
讓我們談?wù)劥a編譯和一些基本概念。請記住,這是一個巨大的主題,需要花費大量時間來掌握,因此在我們的使用案例中,我將只對它進行一般性的介紹。
解釋器vs編譯器
有兩種方法可以使用編譯器和解釋器將代碼轉(zhuǎn)換為機器可以運行的機器語言。
解釋器和編譯器之間的區(qū)別在于,解釋器翻譯您的代碼并逐行執(zhí)行,而編譯器在執(zhí)行之前將所有代碼立即翻譯成機器代碼。
每種方法各有利弊,編譯器啟動很快,但是復(fù)雜且啟動緩慢,解釋器雖然簡單但速度較慢。
話雖如此,有3種方法可以將高級代碼轉(zhuǎn)換為機器代碼并運行它:
- 解釋-使用這種策略,您會有一個解釋器,它逐行執(zhí)行代碼并執(zhí)行(效率不高)。
- 提前進行時間編譯(AOT)-在這里,您需要一個編譯器來首先編譯整個代碼,然后才執(zhí)行它。
- 即時編譯— JOT編譯策略結(jié)合了AOT策略和解釋策略,試圖從兩個方面吸取最大的優(yōu)勢,執(zhí)行動態(tài)編譯,還允許進行某些優(yōu)化,這實際上加快了編譯過程。我們將解釋有關(guān)JIT編譯的更多信息。
大多數(shù)JS引擎使用JIT編譯器,但不是全部。例如,React Native使用的引擎Hermes,沒有使用JIT編譯器。
綜上所述,編譯器采用由解釋器創(chuàng)建的IR并從中生成優(yōu)化的機器代碼。
JIT編譯器
就像我們說的那樣,大多數(shù)JS引擎都使用JIT編譯方法。JIT結(jié)合了AOT策略和解釋,允許進行某些優(yōu)化。讓我們更深入地研究這些優(yōu)化以及編譯器的功能。
JIT編譯優(yōu)化是通過重復(fù)執(zhí)行代碼并對其進行優(yōu)化來完成的。優(yōu)化過程如下:
本質(zhì)上,JIT編譯器通過收集執(zhí)行代碼的概要分析數(shù)據(jù)來獲得反饋,如果它遇到任何熱代碼段(重復(fù)自身的代碼),則該熱段將通過編譯器,然后編譯器將使用此信息重新更優(yōu)化地編譯。
假設(shè)您有一個函數(shù),該函數(shù)返回對象的屬性:
- function load(obj) {
- return obj.x;
- }
看起來簡單嗎?也許對我們來說,但是對于編譯器而言,這并不是一件容易的事。如果編譯器看到一個對象,它不知道任何東西,則它必須檢查屬性x的位置,如果該對象確實具有這樣的屬性,它在內(nèi)存中的位置,在原型鏈中的位置等等。
那么如何優(yōu)化它呢?
為了理解這一點,我們必須知道在機器代碼中,對象及其類型已保存。
假設(shè)我們有一個具有x和y屬性的對象,x是數(shù)字類型,而y是字符串類型。從理論上講,該對象將用如下的機器代碼表示:
如果我們調(diào)用具有相同對象結(jié)構(gòu)的函數(shù),則可以完成優(yōu)化。這意味著屬性將相同且順序相同,但值可以不同,如下所示:
- load({x: 1, y: 'hello'});
- load({x: 5, y: 'world'});
- load({x: 3, y: 'foo'});
- load({x: 9, y: 'bar'});
運作方式如下。調(diào)用該函數(shù)后,優(yōu)化的編譯器將識別出我們正在嘗試調(diào)用已被再次調(diào)用的函數(shù)。
然后,它將繼續(xù)檢查作為參數(shù)傳遞的對象是否具有相同的屬性。
如果是這樣,它將已經(jīng)能夠訪問其在內(nèi)存中的位置,而不用瀏覽原型鏈并完成對未知對象所做的許多其他事情。
本質(zhì)上,編譯器通過優(yōu)化和反優(yōu)化過程運行。
當(dāng)我們運行代碼時,編譯器假定函數(shù)將使用與以前使用的類型相同的類型,因此它將代碼與類型預(yù)先保存在一起。這種類型的代碼稱為優(yōu)化機器代碼。
每次代碼再次調(diào)用相同的函數(shù)時,優(yōu)化的編譯器將嘗試訪問內(nèi)存中的相同位置。
但是由于JS是一種動態(tài)類型的語言,因此在某些時候,我們可能希望將相同的函數(shù)用于不同的類型。在這種情況下,編譯器將執(zhí)行去優(yōu)化過程,并正常地編譯代碼。
總結(jié)一下有關(guān)JIT編譯器的部分,JIT編譯器的工作是通過使用熱代碼段來提高性能,當(dāng)編譯器執(zhí)行之前執(zhí)行的代碼時,它假定類型相同并且使用已生成的優(yōu)化代碼,但是如果類型不同,則JIT會執(zhí)行去優(yōu)化并正常地編譯代碼。
關(guān)于性能的說明
改善應(yīng)用程序性能的一種方法是對不同的對象使用相同的類型。如果您具有相同類型的兩個不同對象,即使值不同,只要屬性具有相同的順序并具有相同的類型,則編譯器會將這兩個對象視為具有相同結(jié)構(gòu)和類型的對象,并且可以更快地訪問它。
例如:
- const obj = { x: 1, a: true, b: 'hey' }
- const obj2 = { x: 7, a: false, b: 'hello' }
從示例中可以看到,我們有兩個具有不同值的不同對象,但是由于屬性的順序和類型相同,因此編譯器將能夠更快地編譯這些對象。
但是,即使有可能以這種方式優(yōu)化代碼,但我認(rèn)為對于性能而言,還有許多重要的事情要做,而這無關(guān)緊要的事情。
在團隊中執(zhí)行這樣的事情也很困難,并且由于引擎非常快,因此總體上看并沒有太大的區(qū)別。
話雖如此,我已經(jīng)看到V8團隊成員推薦了此技巧,所以也許您確實希望有時嘗試遵循它。在可能的情況下遵循它不會對我造成任何傷害,但絕對不會以干凈的代碼和體系結(jié)構(gòu)決策為代價
概要
JS代碼必須在環(huán)境中運行,最常見的是瀏覽器和Node.js。
該環(huán)境需要有一個引擎,該引擎需要采用以人類可讀的語法編寫的JS代碼,然后將其轉(zhuǎn)換為機器代碼。
引擎使用解析器逐行瀏覽代碼,并檢查語法是否正確。如果有任何錯誤,代碼將停止執(zhí)行并引發(fā)錯誤。
如果所有檢查都通過,則解析器將創(chuàng)建一個稱為抽象語法樹(AST)的樹數(shù)據(jù)結(jié)構(gòu)。
AST是一種數(shù)據(jù)結(jié)構(gòu),以樹狀結(jié)構(gòu)表示代碼。通過AST將代碼轉(zhuǎn)換為機器代碼更加容易。
然后,解釋器繼續(xù)進行AST并將其轉(zhuǎn)換為IR,這是機器代碼的抽象,并且是JS代碼和機器代碼之間的中介。IR還可以執(zhí)行優(yōu)化,并且移動性更強。
然后,JIT編譯器通過編譯代碼,獲取動態(tài)反饋并使用該反饋改進編譯過程,從而將生成的IR轉(zhuǎn)換為機器代碼。
原文鏈接:
https://medium.com/coralogix-engineering/how-js-works-behind-the-scenes-the-engine-9f15bba95a15)