圖文帶你看懂JavaScritpt引擎V8與JS執行過程
- 2022 年 7 月 3 日
- 筆記
- javascript, 前端
本篇文章通過圖文為你介紹了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。
- 首先hmtl會被解析為dom樹;
- 然後css會被解析為cssom規則樹;
- 根據dom樹和cssom規則樹構建渲染樹。
- 瀏覽器根據渲染數據進行布局(迴流),此階段瀏覽器計算各節點在頁面中確切位置和大小,也稱自動重排。
- 布局後進行繪製,將內容顯示在螢幕上。
渲染引擎不會等所有html解析完成後再去,構建render tree,而是解析完一部分就顯示一部分。以提高用戶體驗。
V8引擎的執行
V8引擎解析過程概述
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進程有一個堆記憶體。這是存儲靜態數據的地方,包括方法/函數框架、原始值和指向對象的指針。
當然這只是簡化版,實際的情況也會比這複雜得多(如下):
GC垃圾回收
- 引用計數:對象有引用指向它,引用就+1,引用為0就進行回收。但其會產生循環引用。
- 標記清除:早期V8中堆記憶體採用的一種清除演算法,此會有一個根對象,如V8中全局對象。垃圾回收器會定時從根開始去找引用的對象,沒有引用的對象就會回收。可以很好解決循環引用的問題。
JavaScript在記憶體中的執行過程
執行前準備:
-> 首先,js引擎在執行程式碼之前會在在堆記憶體中創建一個全局對象GO(Global Object):
- 該對象在所有作用域可訪問
- 會有
Date
,Math
,SetTimeOut
,SetInterval
,String
,Array
,Number
等 - 內置window屬性指向它本身
-> 然後,JavaScript引擎會在內部創建執行上下文棧ECS(Execution Context Stack),用於執行程式碼調用。
開始執行:
-> 首先會創建一個全局執行環境GEC(Global Execution Context),它包含:在paser轉成AST的過程中,將全局定義的變數,函數加入到GO中,初始為undefined
。(變數作用域提升:全局定義的變數,函數會先入GO再執行)。
並將其入棧到ECS
中。然後逐行執行,進行變數賦值,函數執行操作。
-> 在執行到一個函數時會創建函數執行上下文FEC(Fuction Execution Context),並壓入執行上下文棧ECS,它包含三部分:
- 在解析函數成為AST樹結構時,會創建AO(Activation Object)包含:形參,arguments,函數定義(函數程式碼),函數指向對象,定義邊量;
- 作用域鏈:VO(在函數中就是AO對象) + 父級作用域
- 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被實例化時就會創建它,但是不能被訪問,所以提升不了。(暫時性死區)
案例二閉包
function makeAdder(count){
return function(num){
retrun count + num;
}
}
var add10 = makeAdder(10);
console.log(add10(5));
可以看到在程式碼執行完後,閉包結構中,會一直還有引用在GO中,所以此時不會對其記憶體進行回收。
部分參考及補充:
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