JS是怎樣執行的
問:請你快速說出JS有哪些標籤:
答:單執行緒、指令碼語言、操作DOM吧啦吧啦……
問:好,那你知道JS的執行過程嗎?
答:……-_-||
疑問
以前就在想,為什麼java需要編譯,js就不需要呢?原來程式語言分為靜態語言和動態語言:
靜態語言:比如C++、Go等,都需要提前編譯 (AOT) 成機器碼然後執行,這個過程主要使用編譯器來完成;
動態語言:比如JavaScript、Python等,只在執行時進行編譯執行 (JIT) ,這個過程通過直譯器完成;
v8執行js過程
一、【解析器】Parser生成抽象語法樹(AST)
在Chrome中開始下載Javascript檔案後,Parser就會開始並行在單獨的執行緒上解析程式碼。生成AST的過程可以分為詞義分析和語義分析兩個過程。
1、詞法分析
主要是將字元流(char stream) 轉換成標記流(token stream),字元流就是我們一行一行的程式碼,token是指語法上不能再分的、最小的單個字元或者字串。
為了要詳細看到token stream,我使用了recast 來模擬瀏覽器解析JS。
安裝esprima
npm i recast --save
複製程式碼
新建檔案 recast.js
const recast = require("recast");
const code = 'const LK = 1';
const ast = recast.parse(code);
console.log(ast);
複製程式碼
執行node recast.js
node recast.js
複製程式碼
我擷取了tokens物件,可以看到JS程式碼被切割為type、value的鍵值對。
2、語義分析
語義分析的目的是將分詞得到的語法單元進行一個整體的組合,分析確定語法單元之間的關係。簡單來說,語義分析可以理解成對語句(statement)和表示式(expression)的識別。語義分析是一個遞迴的過程,所以它會將分詞分析出來的陣列轉化成樹形的表達形式(這就是AST中T的由來)。同時,會驗證語法,語法如果存在錯誤的話,會丟擲語法錯誤。
可以看到,短短一句JS程式碼就被解析為樹狀的JSON,各位看官可在https://esprima.org/demo/parse.html線上轉AST。
二、【直譯器】Ignition生成位元組碼
位元組碼是機器碼的抽象,可以看作是小型的構建塊,這些構建塊組合到一起構成任何JavaScript功能。位元組碼比機器碼佔用更小的記憶體,這也是為什麼V8使用位元組碼的一個很重要的原因。位元組碼不能夠直接在處理器上執行,需要通過直譯器將其轉換為機器碼後才能執行。
通過上圖可以看出,Ignition把前一步得到的AST通過位元組碼生成器經過一些列的優化生成位元組碼。 在這個過程中:
- Register Optimizer: 主要是避免暫存器不必要的載入和儲存;
- Peephole Optimizer: 尋找直接碼中可以複用的部分,並進行合併;
- Dead-code Elimination: 刪除無用的程式碼,減少位元組碼的大小;
三、【編譯器】TurboFan
Ignition執行上一步生成的位元組碼,並記錄程式碼執行的次數等資訊,如果同一段程式碼執行了很多次,就會被標記為 “HotSpot”(熱點程式碼),然後把這段程式碼傳送給 編譯器TurboFan,然後TurboFan把它編譯為更高效的機器碼儲存起來,等到下次再執行到這段程式碼時,就會用現在的機器碼替換原來的位元組碼進行執行,這樣大大提升了程式碼的執行效率。 另外,當TurboFan判斷一段程式碼不再為熱點程式碼的時候,會執行去優化的過程,把優化的機器碼丟掉,然後執行過程回到Ignition
從V8的解析、解釋、編譯過程去提高js程式碼效能
一、解析過程
- 刪除多餘的程式碼。
減少瀏覽器請求js檔案時間,減少解析過程及後續流程時間。 - 延遲載入不必要的js【優化首屏載入】。
避免載入和編譯那些會延遲頁面初始顯示的 JavaScript 程式碼,頁面完全載入後,我們可以再開始載入這些功能,以便它們在使用者開始互動時立即可用,Google 建議將此延遲載入以 50 毫秒為單位進行,這樣就不會影響使用者與頁面的互動。 - 正確的書寫js程式碼。
減少語義分析丟擲問題成本。
二、解釋過程
V8採用JIT模式編譯,JIT必須是強型別語言,編譯在執行之前,編譯直接生成CPU能夠執行的二進位制檔案,執行時CPU不需要做任何編譯操作,直接執行,效能最佳。
- 宣告變數時提供預設型別,加快JIT介入。
- 不要輕易改變變數的型別,否則提高編譯時的型別處理成本。
三、編譯過程
始終使用計算複雜度最低的演算法和最佳的資料結構來解決任務。
重寫演算法以獲得相同的結果和更少的計算。
避免遞迴呼叫。
給重複的函式加入變數、計算和呼叫。
分解和簡化數學公式。
使用搜尋陣列:用它們來獲取基於另一個的值,而不是使用 switch/case 語句。
使條件總是更有可能為真,以更好地利用處理器的推測執行。
如果可以,請使用位級運算子替換某些操作,因為這些運算子的處理週期較短。
AST應用
一、babel
babel是一個javascript編譯器,用來將es6語法編譯成es5。
1、解析
通過解析器babylon將程式碼解析成抽象語法樹。
2、解析
通過babel-traverse plugin對抽象語法樹進行深度優先遍歷,遇到需要轉換的,就直接在AST物件上對節點進行新增、更新及移除操作,比如遇到箭頭函式,就轉換成普通函式,最後得到新的AST樹。
3、生成(Generate)
通過babel-generator將AST樹生成es5程式碼。
二、vue模板編譯過程
Vue 提供了 2 個版本,一個是 Runtime + Compiler ,另一個是 Runtime only 的,前者是包含編譯程式碼的,會把編譯的過程放在執行時做,後者是不包含編譯程式碼的,需要藉助 webpack 的vue-loader把模板編譯render函式。不管使用哪個版本,都有一個環節,就是將模板編譯成render函式。
1、解析
將模板字串解析生成 AST,這裡的解析器是vue自己實現的,解析過程中會使用正規表示式對模板順序解析,當解析到開始標籤、閉合標籤、文字的時候都會有相對應的回撥函式執行,來達到構造 AST 樹的目的。
2、解析
vue模板中並不是所有資料都是響應式的,有很多資料是首次渲染後就永遠不會變化的,那麼這部分資料生成的 DOM 也不會變化,我們可以在patch的過程跳過對他們的比對。
此階段會深度遍歷生成的 AST樹,檢測它的每一顆子樹是不是靜態節點,如果是靜態節點則它們生成 DOM 永遠不需要改變,這對執行時對模板的更新起到極大的優化作用。
3、生成(Generate)
通過generate方法,將ast生成render函式。