這篇要來複習JavaScript引擎和瀏覽器執行時期運作過程。
Browser & JavaScript Engine
先簡單認識瀏覽器在JavaScript的執行時期會有哪些東西在運作,再來認識JavaScript引擎。
透過上圖可一目瞭然瀏覽器執行時期主要有三個東西在運作,分別是JavaScript引擎(engine)、Web APIs、Callback Queue。
其中的運作核心就是JavaScript引擎,用來解析、編譯並執行JavaScript程式碼。
JavaScript Engine
JavaScript引擎解析與執行程式碼的過程如下:
首先,程式碼(code)被JavaScript引擎讀取會先解析(parsing)成一種特別的資料結構AST(abstract synthetic tree),也就是說程式碼的每一部分都會解析並轉成AST樹結構的節點,因為AST不是這篇的重點這部分會略過。
接下來轉成AST結構的程式碼會進入「即時編譯」(just-in-time compilation)階段,然後編譯後的程式碼才會在引擎的主執行緒(call stask)執行,並且會在執行的同時「最佳化(optiomization)」程式碼、意即優化程式碼並加快執行速度。
要注意的是,只有「執行程式碼」這個步驟會在引擎的主執行緒處理,解析、即時編譯和最佳化都會在主執行緒以外的其他執行緒處理,沒辦法透過JavaScript程式碼去改變這些步驟。
Just-In-Time Compilation
這裡稍微暫停一下解釋何謂即時編譯?
在電腦科學領域,我們所寫的程式語言,像是JavaScript、Python、C++、Java等可以算是一種人類可大致讀懂的高階語言,但是電腦本身其實看不懂這種高階語言,必須先「翻譯」成機器語言(machine language),才可以被電腦所讀懂。
而「翻譯」成機器語言的方式大致有這幾種,編譯(compilation)、直譯(interpretation)和現代JavaScript引擎所採用的即時編譯(just-in-time compilation)。
「編譯(compilation)」是指先將程式碼翻譯並儲存成一種可被電腦直接讀取並執行的可攜檔(portable file),然後電腦再執行這個可攜檔。所謂可攜檔正如其名指該檔案不一定要在原先編譯的電腦讀取執行,而是在任一擁有讀取該類型可攜檔工具的電腦都可以讀取執行,「編譯」的優點在於因為已經事先將程式碼翻譯成機器語言,所以執行速度會比較快。
「直譯(interpretation)」則是指成程式碼在執行之前才由直譯器翻譯、執行,簡單來說就「直譯」是邊讀取、邊翻譯執行。相較於編譯,直譯在執行程式碼的速度上會比較慢。(編譯和直譯可以想成是要把一份英文文章翻譯成中文,「編譯」就是事先把英文文章全部翻成一篇中文文章,然後中文為母語的人可以直接閱讀理解這篇中文文章,而「直譯」就是邊看一句英文邊翻譯成一句中文,所以在閱讀與理解速度上會比直接閱讀中文文章來的慢。)
「即時編譯(Just-In-Time compilation)」則是現代JavaScript引擎執行JavaScript程式碼的方式。可能過去或平常會聽到JavaScript引擎是以直譯方式在執行程式碼,但直譯程式碼的速度對於現在的應用程式來說實在是太慢了,因此現代JavaScript引擎已經不再是以傳統的直譯方式翻譯程式碼,而是採用所謂的「即時編譯」。即時編譯程式碼的方式類似於「編譯」,會先將檔案整個程式碼直接翻譯成機器語言,但不會像「編譯」方式一樣翻譯後產生一個執行檔,而是直接讓JavaScript引擎執行(execution)翻譯後的程式碼。
由於即時編譯一開始並不會以最佳的方式翻譯程式碼,因此以Google chrome的v8引擎為例,引擎在執行翻譯後程式碼的同時也會"最佳化"(optimization)程式碼、改善程式碼品質,提升程式碼的執行速度。
JS Runtime Behavior in Browser
回頭來看JavaScript執行時期的瀏覽器長什麼樣子,以下會結合這張圖整理JavaScript引擎、Web APIs和callback queue的功能。
JavaScript Engine
JavaScript引擎主要包含兩個部分,分別是heap和call stack。Heap可以看作是一塊沒有限制的記憶體區域,用來儲存objects;而call stack,或說主執行緒(main thread),則是JavaScript引擎主要執行程式碼和儲存變數的地方,或者可以更精確地說,程式碼執行和變數儲存都會在call stack的execution context中處理。
Web APIs
Web APIs包含常見的DOM、fetch API、timer(setTimeOut)抖,但是Web APIs不是JavaScript引擎的一員,而是瀏覽器的一部分,不過JavaScript可以透過 window object 去操作Web APIs。
Callback Queue
迴呼佇列(callback queue)則是用來儲存回呼(callback)函式、事件的地方。
佇列(queue)是一種資料結構,儲存進去的資料是以先進先出(first-in first-out)的方式在處理,就如同平常排隊買票一樣,排在前面的人就可以先買票。
迴呼佇列會透過一種稱為「事件迴圈(event loop)」的機制,在JavaScript引擎的主執行緒(也就是call stack)清空除了global execution context以外的execution contexts以後,將佇列內儲存的迴呼事件放進引擎內的主執行緒去執行。
事件迴圈是一個非常重要的機制,因為JavaScript是一種只有單一執行緒(single thread)、非阻塞(non-blocking)、並行(concurrency)的語言,所以JavaScript沒辦法像其他語言使用多個執行序平行處理程式碼,但為了達到「並行(concurrency)」處理的目的(也就是"同時處理多個任務"),在JavaScript引擎主執行緒(call stack)處理程式碼的同時,瀏覽器會在背景(背後)處理非同步(asynchronous)任務,等到非同步任務完成後就會放進迴呼佇列等待JavaScript引擎的主執行緒被清空(意即除了global execution context以外的execution contexts都執行完被移除),然後事件迴圈會從迴呼佇列的頭一個一個取出迴呼事件放進主執行緒繼續接下來的動作。
Execution contexts和事件迴圈機制在其他文章有更清楚的說明,但關於JavaScript引擎和執行時期瀏覽器的大致運作方式應該到這裡結束@@
References
The Complete JavaScript Course 2023: From Zero to Expert!