圖文帶你看懂JavaScritpt引擎V8與JS執行過程

本篇文章通過圖文為你介紹了V8引擎大概的執行過程,你可以了解到程式碼是從從掃描器Scaner變成tokens,從解析器Parser變成AST,從解釋器變成位元組碼等等。以及JavaScript程式碼在執行的過程中,它在記憶體的情況是如何變化的,讓你從更加底層的角度去理解你的js程式碼是如何運行的。了解這些後你就能從更加底層的角度去理解var的變數提升,閉包的形成等了。

瀏覽器原理

瀏覽器內核與js引擎

瀏覽器內核又稱「排版引擎」,「渲染引擎」,「瀏覽器引擎」,叫法很多,簡單來說乾的活就是將程式碼(HTML,XML,CSS,圖片等)解析排版布局後輸出到顯示器讓你看到。

JavaScript引擎是一個專門處理JavaScript腳本的虛擬機,一般會附帶在網頁瀏覽器之中。

主流瀏覽器內核與js引擎:

瀏覽器 內核 js引擎
Safari WebKit javaScriptCore
Chrome Blink V8
firefox Gecko SpiderMonkey…

瀏覽器渲染過程概述

輸入網址,伺服器返回html,瀏覽器內核開始解析html,遇到link 等之類則會暫停,去下載對應的css或者js。

  1. 首先hmtl會被解析為dom樹;
  2. 然後css會被解析為cssom規則樹;
  3. 根據dom樹和cssom規則樹構建渲染樹。
  4. 瀏覽器根據渲染數據進行布局(迴流),此階段瀏覽器計算各節點在頁面中確切位置和大小,也稱自動重排。
  5. 布局後進行繪製,將內容顯示在螢幕上。

渲染引擎不會等所有html解析完成後再去,構建render tree,而是解析完一部分就顯示一部分。以提高用戶體驗。

V8引擎的執行

V8引擎解析過程概述

image
BLinK內核遇到js程式碼後,會以流的形式傳遞給v8,然其開始工作:

  • 首先接收到流後,會有掃描器Scanner對其進行詞法分析將程式碼轉化為tokens
  • 然後解析器parser將其轉換為AST抽象語法樹。
  • 再由解釋器ignition(圖中閃電部分)生成位元組碼再進行執行。

Parser再探:

Parser解析的時並不會進行全量解析(全部解析1.耗時間;2.解析後的位元組碼需放入記憶體耗記憶體),而是有延遲解析的策略,也就是一種按需解析給方案,( 理解:首先會Perpaser會解析出所需的最少限度的內容,比如內部有未調用的函數,則解析出函數聲明,當調用時則paser對該函數進行完整的解析 )。

Ignition再探:

Ignition關注的是減少 V8 的記憶體開銷,會進行執行前的優化工作。它會將AST進行分析將多次調用的函數標記為熱點函數 交由TurboFan進行編譯生成優化後的機器碼(優化,方便快速調用)執行。而單次調用的函數則會被生成位元組碼再做執行。所以它也會有編譯過程的,所以也有人對JS是否是解釋型語言有爭議。而正如最新的MDN上的文檔說的JavaScript是一種具有函數優先的輕量級,解釋型或即時編譯型的程式語言,應該是最準確的吧。

V8記憶體模型

V8的記憶體主要分為堆和棧兩部分,用以執行程式碼,和JVM有點類似😂。
堆: 這是最大的記憶體塊,也是垃圾回收(GC)發生的地方。
棧: 每個V8進程有一個堆記憶體。這是存儲靜態數據的地方,包括方法/函數框架、原始值和指向對象的指針。
image

當然這只是簡化版,實際的情況也會比這複雜得多(如下):
image

GC垃圾回收

  • 引用計數:對象有引用指向它,引用就+1,引用為0就進行回收。但其會產生循環引用。
  • 標記清除:早期V8中堆記憶體採用的一種清除演算法,此會有一個根對象,如V8中全局對象。垃圾回收器會定時從根開始去找引用的對象,沒有引用的對象就會回收。可以很好解決循環引用的問題。

JavaScript在記憶體中的執行過程

執行前準備:

-> 首先,js引擎在執行程式碼之前會在在堆記憶體中創建一個全局對象GO(Global Object):

  1. 該對象在所有作用域可訪問
  2. 會有 Date,Math,SetTimeOut,SetInterval,String,Array,Number
  3. 內置window屬性指向它本身

-> 然後,JavaScript引擎會在內部創建執行上下文棧ECS(Execution Context Stack),用於執行程式碼調用。

開始執行:

-> 首先會創建一個全局執行環境GEC(Global Execution Context),它包含:在paser轉成AST的過程中,將全局定義的變數,函數加入到GO中,初始為undefined。(變數作用域提升:全局定義的變數,函數會先入GO再執行)。

並將其入棧到ECS中。然後逐行執行,進行變數賦值,函數執行操作。

-> 在執行到一個函數時會創建函數執行上下文FEC(Fuction Execution Context),並壓入執行上下文棧ECS,它包含三部分:

  1. 在解析函數成為AST樹結構時,會創建AO(Activation Object)包含:形參,arguments,函數定義(函數程式碼),函數指向對象,定義邊量
  2. 作用域鏈:VO(在函數中就是AO對象) + 父級作用域
  3. this綁定的值。

準備執行【創建GO 創建ECS 解析全局變數,函數(若變數初始為undefined,若函數則創建函數對象進行存儲)】-> 執行程式碼【遇到函數調用 -> 創建其函數的AO對象 -> 創建其函數執行上下文 -> 執行函數內部程式碼】

註:在最新的ECMA標準中,變數對象VO,該為了變數環境VE,其可以不為對象,只要其能存儲環境記錄,其包含的內容也有些差異。

結合程式碼示例進行分析

案例一

var name = "shinna_mashiro";
foo(666);
function foo(num){
    console.log(m);
    var m = 10;
    var n = 20;
    function bar(){
        console.log(name)
    }
    bar()
}

這是通過var聲明的變數,而通過let,const聲明的變數ECMA262對它們是這麼描述的:The variables(let 或 const)are created when their containing Lexical Environment is instantiate but may not be accessed in any way until the variable’s LexicalBinding is evaluated. 這些變數會被創建在包含他們的詞法環境(VE -> VO)被實例化時,但是是不可以訪問的,直到詞法綁定被執行。也就是在FEC創建的時候,VE被實例化時就會創建它,但是不能被訪問,所以提升不了。(暫時性死區)

image

image

image

案例二閉包

function makeAdder(count){
    return function(num){
        retrun count + num;
    }
}
var add10 = makeAdder(10);
console.log(add10(5));

可以看到在程式碼執行完後,閉包結構中,會一直還有引用在GO中,所以此時不會對其記憶體進行回收。
image

部分參考及補充:
1.Visualizing memory management in V8 Engine (JavaScript, NodeJS, Deno, WebAssembly)://deepu.tech/memory-management-in-v8/
2.全面分析總結JS記憶體模型://segmentfault.com/a/1190000021996331
3.V8引擎詳解://juejin.cn/post/6844904146798116871
4.JavaScript到底是解釋型語言還是編譯型語言?://segmentfault.com/a/1190000013126460
5.Blazingly fast parsing, part 2: lazy parsing: //v8.dev/blog/preparser