<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Js是怎樣運行起來的?

          共 12071字,需瀏覽 25分鐘

           ·

          2021-07-01 21:04

          前言

          不知道大家有沒有想過這樣一個問題,我們所寫的 JavaScript 代碼是怎樣被計算機認識并且執(zhí)行的呢?這中間的過程具體是怎樣的呢?有的同學可能已經(jīng)知道,Js 是通過 Js 引擎運行起來的,那么

          • 什么是 Js 引擎?
          • Js 引擎是怎樣編譯執(zhí)行和優(yōu)化 Js 代碼的?

          Js 引擎有很多種,比如 Chrome 使用的 V8 引擎,Webkit 使用的是 JavaScriptCore,React Native 使用的是 Hermes。今天我們主要來分析一下比較主流的 V8 引擎是怎樣運行 Js 的。

          V8 引擎

          在介紹 V8 引擎的概念之前,我們先來回顧一下編程語言。編程語言可以分為機器語言、匯編語言、高級語言。

          • 機器語言:由 0 和 1 組成的二進制碼,對于人類來說是很難記憶的,還要考慮不同 CPU 平臺的兼容性。
          • 匯編語言:用更容易記憶的英文縮寫標識符代替二進制指令,但還是需要開發(fā)人員有足夠的硬件知識。

          • 高級語言:更簡單抽象且不需要考慮硬件,但是需要更復雜、耗時更久的翻譯過程才能被執(zhí)行。

          到了這里我們知道,高級語言一定要轉(zhuǎn)化為機器語言才能被計算機執(zhí)行,而且越高級的語言轉(zhuǎn)化的時間越久。高級語言又可以分為解釋型語言、編譯型語言。

          • 編譯型語言:需要編譯器進行一次編譯,被編譯過的文件可以多次執(zhí)行。如 C++、C 語言。
          • 解釋型語言:不需要事先編譯,通過解釋器一邊解釋一邊執(zhí)行。啟動快,但執(zhí)行慢。

          我們知道 JavaScript 是一門高級語言,并且是動態(tài)類型語言,我們在定義一個變量時不需要關(guān)心它的類型,并且可以隨意的修改變量的類型。而在像 C++這樣的靜態(tài)類型語言中,我們必須提前聲明變量的類型并且賦予正確的值才行。也正是因為 JavaScript 沒有像 C++那樣可以事先提供足夠的信息供編譯器編譯出更加低級的機器代碼,它只能在運行階段收集類型信息,然后根據(jù)這些信息進行編譯再執(zhí)行,所以 JavaScript 也是解釋型語言。

          這也就意味著 JavaScript 要想被計算機執(zhí)行,需要一個能夠快速解析并且執(zhí)行 JavaScript 腳本的程序,這個程序就是我們平時所說的 JavaScript 引擎。這里我們給出 V8 引擎的概念:V8 是 Google 基于 C++ 編寫的開源高性能 Javascript 與 WebAssembly 引擎。用于 Google Chrome(Google 的開源瀏覽器) 以及 Node.js 等。

          CPU 是如何執(zhí)行機器指令的?

          將高級語言轉(zhuǎn)化為機器語言之后,CPU 又是怎樣執(zhí)行的呢?我們以一段 C 代碼為例:

          int main()

          {

              int x = 1;

              int y = 2;

              int z = x + y;

              return z;

          }}

          先來看一下以上代碼被轉(zhuǎn)換為機器語言是什么樣子。下圖左側(cè)是用十六進制表示的二進制機器碼,中間部分是匯編代碼,右側(cè)是指令的含義。

          CPU 執(zhí)行機器指令的流程

          • 首先程序在執(zhí)行之前會被裝進內(nèi)存。
          • 系統(tǒng)會將二進制代碼中的第一條指令的地址寫入到 PC 寄存器中。
          • CPU 根據(jù) PC 寄存器中的地址,從內(nèi)存中取出指令。
          • 將下一條指令的地址更新到 PC 寄存器中。

          • 分析當前取出指令,并識別出不同的類型的指令,以及各種獲取操作數(shù)的方法。

          • 加載指令:從內(nèi)存中復制指定長度的內(nèi)容到通用寄存器中,并覆蓋寄存器中原來的內(nèi)容。

          • 存儲指令:將寄存器中的內(nèi)容復制到內(nèi)存某個位置,并覆蓋掉內(nèi)存中的這個位置上原來的內(nèi)容。

          上圖中 movl 指令后面的 %ecx 就是寄存器地址,-8(%rbp) 是內(nèi)存中的地址,這條指令的作用是將寄存器中的值拷貝到內(nèi)存中。

          • 更新指令:復制兩個寄存器中的內(nèi)容到 ALU 中,也可以是一塊寄存器和一塊內(nèi)存中的內(nèi)容到 ALU 中,ALU 將兩個字相加,并將結(jié)果存放在其中的一個寄存器中,并覆蓋該寄存器中的內(nèi)容。

          ...

          • 執(zhí)行指令完畢,進入下一個 CPU 時鐘周期。

          V8 引擎的編譯流水線

          接下來我們先從宏觀的角度來看一下 V8 是怎么執(zhí)行 JavaScript 代碼的,然后再對每一步進行分析。

          • 初始化基礎(chǔ)環(huán)境;
          • 解析源碼生成 AST 和作用域;
          • 依據(jù) AST 和作用域生成字節(jié)碼;
          • 解釋執(zhí)行字節(jié)碼;監(jiān)聽熱點代碼;
          • ...

          完整的分析一段 JavaScript 代碼是怎樣被執(zhí)行的

          1、初始化基礎(chǔ)環(huán)境

          V8 執(zhí)行 Js 代碼是離不開宿主環(huán)境的,V8 的宿主可以是瀏覽器,也可以是 Node.js。下圖是瀏覽器的組成結(jié)構(gòu),其中渲染引擎就是平時所說的瀏覽器內(nèi)核,它包括網(wǎng)絡(luò)模塊,Js 解釋器等。當打開一個渲染進程時,就為 V8 初始化了一個運行時環(huán)境。運行時環(huán)境為 V8 提供了堆空間,棧空間、全局執(zhí)行上下文、消息循環(huán)系統(tǒng)、宿主對象及宿主 API 等。V8 的核心是實現(xiàn)了 ECMAScript 標準,此外還提供了垃圾回收器等內(nèi)容。

          2、解析源碼生成 AST 和作用域

          基礎(chǔ)環(huán)境準備好之后,接下來就可以向 V8 提交要執(zhí)行的 JavaScript 代碼了。首先 V8 會接收到要執(zhí)行的 JavaScript 源代碼,不過這對 V8 來說只是一堆字符串,V8 并不能直接理解這段字符串的含義,它需要結(jié)構(gòu)化這段字符串。



          function add(x, y{

            var z = x+y

            return z

          }

          console.log(add(12))

          比如針對如上源代碼,V8 首先通過解析器(parser)解析成如下的抽象語法樹 AST:



          [generating bytecode for functionadd]

          --- AST ---

          FUNC at 12

          KIND 0

          LITERAL ID 1

          SUSPEND COUNT 0

          NAME "add"

          PARAMS

          . . VAR (0x7fa7bf8048e8) (mode = VAR, assigned = false) "x"

          . . VAR (0x7fa7bf804990) (mode = VAR, assigned = false) "y"

          DECLS

          . . VARIABLE (0x7fa7bf8048e8) (mode = VAR, assigned = false) "x"

          . . VARIABLE (0x7fa7bf804990) (mode = VAR, assigned = false) "y"

          . . VARIABLE (0x7fa7bf804a38) (mode = VAR, assigned = false) "z"

          BLOCK NOCOMPLETIONS at -1

          . . EXPRESSION STATEMENT at 31

          . . . INIT at 31

          . . . . VAR PROXY local[0] (0x7fa7bf804a38) (mode = VAR, assigned = false) "z"

          . . . . ADD at 32

          . . . . . VAR PROXY parameter[0] (0x7fa7bf8048e8) (mode = VAR, assigned = false) "x"

          . . . . . VAR PROXY parameter[1] (0x7fa7bf804990) (mode = VAR, assigned = false) "y"

          RETURN at 37

          . . VAR PROXY local[0] (0x7fa7bf804a38) (mode = VAR, assigned = false) "z"

          V8 在生成 AST 的同時,還生成了 add 函數(shù)的作用域:



          Global scope:

          function add (x, y// (0x7f9ed7849468) (12, 47)

            // will be compiled

            // 1 stack slots

            // local vars:

            VAR y;  // (0x7f9ed7849790) parameter[1], never assigned

            VAR z;  // (0x7f9ed7849838) local[0], never assigned

            VAR x;  // (0x7f9ed78496e8) parameter[0], never assigned

          }

          在解析期間,所有函數(shù)體中聲明的變量和函數(shù)參數(shù),都被放進作用域中,如果是普通變量,那么默認值是 undefined,如果是函數(shù)聲明,那么將指向?qū)嶋H的函數(shù)對象。在執(zhí)行階段,作用域中的變量會指向堆和棧中相應的數(shù)據(jù)。

          3、依據(jù) AST 和作用域生成字節(jié)碼

          生成了作用域和 AST 之后,V8 就可以依據(jù)它們來生成字節(jié)碼了。AST 之后會被作為輸入傳到字節(jié)碼生成器 (BytecodeGenerator),這是 Ignition 解釋器中的一部分,用于生成以函數(shù)為單位的字節(jié)碼。



          [generated bytecode for functionadd (0x079e0824fdc1 <SharedFunctionInfo add>)]

          Parameter count 3

          Register count 2

          Frame size 16

                   0x79e0824ff7a @    0 : a7                StackCheck

                   0x79e0824ff7b @    1 : 25 02             Ldar a1

                   0x79e0824ff7d @    3 : 34 03 00          Add a0, [0]

                   0x79e0824ff80 @    6 : 26 fb             Star r0

                   0x79e0824ff82 @    8 : 0c 02             LdaSmi [2]

                   0x79e0824ff84 @   10 : 26 fa             Star r1

                   0x79e0824ff86 @   12 : 25 fb             Ldar r0

                   0x79e0824ff88 @   14 : ab                Return

          Constant pool (size = 0)

          Handler Table (size = 0)

          Source Position Table (size = 0)

          4、解釋執(zhí)行字節(jié)碼

          和 CPU 執(zhí)行二進制機器代碼類似:使用內(nèi)存中的一塊區(qū)域來存放字節(jié)碼;使通用寄存器用來存放一些中間數(shù)據(jù);PC 寄存器用來指向下一條要執(zhí)行的字節(jié)碼;棧頂寄存器用來指向當前的棧頂?shù)奈恢谩?img data-ratio="0.4001751313485114" src="https://filescdn.proginn.com/f284b88fdb7203ffd4e6da1537e41555/4a0651d2fcde7faab3b3b7b49f5df104.webp" data-type="jpeg" data-w="2284" style="margin-right: auto;margin-left: auto;width: 100%;border-radius: 5px;display: block;margin-bottom: 15px;">

          • StackCheck 字節(jié)碼指令就是檢查棧是否達到了溢出的上限。
          • Ldar 表示將寄存器中的值加載到累加器中。

          • Add 表示寄存器加載值并將其與累加器中的值相加,然后將結(jié)果再次放入累加器。

          • Star 表示 把累加器中的值保存到某個寄存器中。

          • Return 結(jié)束當前函數(shù)的執(zhí)行,并將控制權(quán)傳回給調(diào)用方。返回的值是累加器中的值。

          5、即時編譯

          在解釋器 Ignition 執(zhí)行字節(jié)碼的過程中,如果發(fā)現(xiàn)有熱點代碼(HotSpot),比如一段代碼被重復執(zhí)行多次,這種就稱為熱點代碼,那么后臺的編譯器 TurboFan 就會把該段熱點的字節(jié)碼編譯為高效的機器碼,然后當再次執(zhí)行這段被優(yōu)化的代碼時,只需要執(zhí)行編譯后的機器碼就可以了,這樣就大大提升了代碼的執(zhí)行效率。這種字節(jié)碼配合解釋器和編譯器的技術(shù)被稱為即時編譯(JIT)。

          V8 的優(yōu)化策略

          下面我們來看一下,V8 為了提升解析和執(zhí)行 Js 的速度,做了哪些優(yōu)化。由于篇幅關(guān)系,這里只介紹 5 個優(yōu)化點。

          1、重新引入字節(jié)碼

          早期的 V8 團隊認為先生成字節(jié)碼再執(zhí)行字節(jié)碼的方式會降低代碼的執(zhí)行效率,于是直接將 JavaScript 代碼編譯成機器代碼。這樣做帶來的問題有兩點,一是需要較長的編譯時間,二是產(chǎn)生的二進制機器碼需要占用較大的內(nèi)存空間。使用字節(jié)碼的話雖然犧牲了一點執(zhí)行效率,但是節(jié)省了內(nèi)存空間并且降低了編譯時間。此外,字節(jié)碼也降低了 V8 代碼的復雜度,使得 V8 移植到不同的 CPU 架構(gòu)平臺更加容易。這是因為統(tǒng)一將字節(jié)碼轉(zhuǎn)換為不同平臺的二進制代碼要比編譯器編寫不同 CPU 體系的二進制代碼更加容易。

          2、延遲解析

          通過 V8 的編譯流程我們可以看出,V8 執(zhí)行 JavaScript 代碼需要經(jīng)過編譯和執(zhí)行兩個階段。

          • 編譯過程:是指 V8 將 JavaScript 代碼轉(zhuǎn)換為字節(jié)碼,或者二進制機器代碼的階段。
          • 執(zhí)行階段:是指解釋器解釋執(zhí)行字節(jié)碼,或者是 CPU 直接執(zhí)行二進制機器代碼的階段。

          V8 并不會一次性將所有的 JavaScript 解析為中間代碼,這主要是基于以下兩點:

          • 如果一次解析和編譯所有的 JavaScript 代碼,過多的代碼會增加編譯時間,這會嚴重影響到首次執(zhí)行 JavaScript 代碼的速度,讓用戶感覺到卡頓。
          • 其次,解析完成的字節(jié)碼和編譯之后的機器代碼都會存放在內(nèi)存中,如果一次性解析和編譯所有 JavaScript 代碼,那么這些中間代碼和機器代碼將會一直占用內(nèi)存。

          延遲解析是指解析器在解析的過程中,如果遇到函數(shù)聲明,那么會跳過函數(shù)內(nèi)部的代碼,并不會為其生成 AST 和字節(jié)碼。

          3、隱藏類

          我們可以結(jié)合一段代碼來分析下隱藏類是怎么工作的:

          let point = {x:100,y:200}

          當 V8 執(zhí)行到這段代碼時,會先為 point 對象創(chuàng)建一個隱藏類,在 V8 中,把隱藏類又稱為 map,每個對象都有一個 map 屬性,其值指向內(nèi)存中的隱藏類。隱藏類描述了對象的屬性布局,它主要包括了屬性名稱和每個屬性所對應的偏移量,比如 point 對象的隱藏類就包括了 x 和 y 屬性,x 的偏移量是 4,y 的偏移量是 8。有了隱藏類之后,那么當 V8 訪問某個對象中的某個屬性時,就會先去隱藏類中查找該屬性相對于它的對象的偏移量,有了偏移量和屬性類型,V8 就可以直接去內(nèi)存中取出對應的屬性值,而不需要經(jīng)歷一系列的查找過程,那么這就大大提升了 V8 查找對象的效率。

          4、快屬性與慢屬性

          當我們在控制臺輸入如下代碼時:

          function Foo({

              this[100] = 'test-100'

              this[1] = 'test-1'

              this["B"] = 'bar-B'

              this[50] = 'test-50'

              this[9] =  'test-9'

              this[8] = 'test-8'

              this[3] = 'test-3'

              this[5] = 'test-5'

              this["A"] = 'bar-A'

              this["C"] = 'bar-C'

          }

          var bar = new Foo()





          for(key in bar){

              console.log(`index:${key}  value:${bar[key]}`)

          }

          打印出來的結(jié)果如下:

          index:1  value:test-1

          index:3  value:test-3

          index:5  value:test-5

          index:8  value:test-8

          index:9  value:test-9

          index:50  value:test-50

          index:100  value:test-100

          index:B  value:bar-B

          index:A  value:bar-A

          index:C  value:bar-C

          之所以出現(xiàn)這樣的結(jié)果,是因為在 ECMAScript 規(guī)范中定義了數(shù)字屬性應該按照索引值大小升序排列,字符串屬性根據(jù)創(chuàng)建時的順序升序排列。

          • 數(shù)字屬性稱為排序?qū)傩裕?V8 中被稱為 elements。
          • 字符串屬性就被稱為常規(guī)屬性,在 V8 中被稱為 properties。

          下面我們執(zhí)行這樣一段代碼,看一看當對象中的屬性數(shù)目發(fā)生變化時,其在內(nèi)存中結(jié)構(gòu)是怎樣變化的。



          function Foo(property_num,element_num{

              //添加排序?qū)傩?/span>

              for (let i = 0; i < element_num; i++) {

                  this[i] = `element${i}`

              }

              //添加常規(guī)屬性

              for (let i = 0; i < property_num; i++) {

                  let ppt = `property${i}`

                  this[ppt] = ppt

              }

          }

          var bar = new Foo(10,10)

          將 Chrome 開發(fā)者工具切換到 Memory 標簽,然后點擊左側(cè)的小圓圈就可以捕獲以上代碼的內(nèi)存快照,最終截圖如下所示:將創(chuàng)建的對象屬性的個數(shù)調(diào)整到 20 個

          var bar2 = new Foo(20,10)

          總結(jié):當對象中的屬性過多時,或者存在反復添加或者刪除屬性的操作,那么 V8 就會將線性的存儲模式(快屬性)降級為非線性的字典存儲模式(慢屬性),這樣雖然降低了查找速度,但是卻提升了修改對象的屬性的速度。

          5、內(nèi)聯(lián)緩存

          我們再來看一段這樣的代碼。

          function loadX(o{

              o.y = 4

              return o.x

          }

          var o = { x1,y:3}

          var o1 = { x3 ,y:6}

          for (var i = 0; i < 90000; i++) {

              loadX(o)

              loadX(o1)

          }

          通常 V8 獲取 o.x 的流程是這樣的:查找對象 o 的隱藏類,再通過隱藏類查找 x 屬性偏移量,然后根據(jù)偏移量獲取屬性值,在這段代碼中 loadX 函數(shù)會被反復執(zhí)行,那么獲取 o.x 流程也需要反復被執(zhí)行。為了提升對象的查找效率。V8 執(zhí)行的策略就是使用內(nèi)聯(lián)緩存 (Inline Cache),簡稱為 IC。IC 會為每個函數(shù)維護一個反饋向量 (FeedBack Vector),反饋向量記錄了函數(shù)在執(zhí)行過程中的一些關(guān)鍵的中間數(shù)據(jù)。然后將這些數(shù)據(jù)緩存起來,當下次再次執(zhí)行該函數(shù)時,V8 就可以直接利用這些中間數(shù)據(jù),節(jié)省了再次獲取這些數(shù)據(jù)的過程。V8 會在反饋向量中為每個調(diào)用點分配一個插槽(Slot),比如 o.y = 4 和 return o.x 這兩段就是調(diào)用點 (CallSite),因為它們使用了對象和屬性。每個插槽中包括了插槽的索引 (slot index)、插槽的類型 (type)、插槽的狀態(tài) (state)、隱藏類 (map) 的地址、還有屬性的偏移量,比如上面這個函數(shù)中的兩個調(diào)用點都使用了對象 o,那么反饋向量兩個插槽中的 map 屬性也都是指向同一個隱藏類的,因此這兩個插槽的 map 地址是一樣的。通過內(nèi)聯(lián)緩存策略,就能夠提升下次執(zhí)行函數(shù)時的效率,但是這有一個前提,那就是多次執(zhí)行時,對象的形狀是固定的,如果對象的形狀不是固定的,這意味著 V8 為它們創(chuàng)建的隱藏類也是不同的。面對這種情況,V8 會選擇將新的隱藏類也記錄在反饋向量中,同時記錄屬性值的偏移量,這時,反饋向量中的一個槽里就會出現(xiàn)包含了多個隱藏類和偏移量的情況,如果超過 4 個,那么 V8 會采取 hash 表的結(jié)構(gòu)來存儲。講到這里我的分享就結(jié)束了,如果有不足之處歡迎大家多多批評指正。



          參考鏈接

          https://www.cnblogs.com/nickchen121/p/10722720.html

          https://v8.dev/docs

          https://juejin.cn/post/6844904161163608078

          https://time.geekbang.org/column/article/211682

          https://www.jianshu.com/p/e4a75cb6f268


          以上便是本次分享的全部內(nèi)容,希望對你有所幫助^_^

          喜歡的話別忘了 分享、點贊、收藏 三連哦~。

          歡迎關(guān)注公眾號 前端Sharing 收貨大廠一手好文章~



          瀏覽 36
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  av永久免费 | 狂野欧美性交 | 99热精品在线免费观看 | 无码高清毛片 | 国产一区二区视频在线 |