Overview
本篇文章會包含以下主題,從了解Javascript的執行環境及資料結構到事件迴圈詳細的運作流程,也算是涵蓋從初階到進階Javascript開發者都必須知道的內容。
- Javascript Engine
- Memory Heap
- Call Stack
- Javascript Runtime
- 同步與非同步
- 事件迴圈 Event Loop
- Macrotask queue vs Microtask queue
Javascript Engine
電腦上真正執行運算工作的是CPU,但CPU只能理解機器語言(Machine code),無法直接讀懂Javascript這種高階語言,Javascript Engine的功能就是把Javascript翻譯成機器語言,如此一來我們就不用直接撰寫可讀性差的機器語言。
Engine裡包含很多模組,本篇文章只會介紹其中的Interpreter, Compiler, Memory Heap以及Call Stack。
Engine會透過兩個步驟翻譯Javascript,第一個是Interpreter會先將js轉成Bytecode,第二個是Compiler會將Bytecode轉成Machine Code同時Profiler會一起進行優化。
最常聽見的V8 Engine是由Google開發,被使用在Chrome以及Node.js上,但其實還有其他很多Engine,只要符合ECMAScript制定的標準大家都可以開發自己的Javascript Engine.
Memory Heap
主要有以下功能:
- 分配記憶體空間給變數使用,分配好之後會得到一個位址
- 將變數的值寫進配置好的記憶體空間
- 使用位址從記憶體空間讀取變數的值
- 釋放不再使用的記憶體空間(Garbage Collection)
舉例來說,Memory Heap就像是一個抽屜櫃,每個有放東西的抽屜都會有一個編號,程式會用這個編號去找東西。
抽屜櫃→Memory Heap
抽屜→記憶體空間
東西→變數的值
編號→記憶體位址(reference)
Call Stack
Stack是一個先進後出(FILO, First In Last Out)類似罐子的結構,當程式呼叫函式(Call function)時會把函式放進stack中,直到函式執行結束return後再將函式從stack中取出。
Call Stack的最底層一定會有一個Global Execution Context,提供其他函式存取global object以及this
關鍵字。
因為Javascript只有一個Call Stack程式中所有的任務都會在這個Stack中執行,所以被稱為Single Thread,這個設計讓他同一時間只有辦法執行一件任務,無法同時執行多件任務,也就是所謂的Synchronous。
Javascript雖然只有一個Engine在執行任務,但通常背後還有Runtime在非同步的執行任務。
Javascript Runtime
Runtime指的是在執行時期提供Global Object給JS Engine讓它有能力與執行環境互動。
在Browser環境中Runtime就是Web APIs,JS Engine可以透過window
object來使用Web APIs,透過Web APIs執行非同步的任務,例如操作DOM, AJAX, setTimeout等。
在Node.js環境中Runtime就是Global APIs,JS Engine可以透過global
object來使用各種built-in API,例如Buffer, process, require等。
詳細解釋同步與非同步
同步(Synchronous)指的是程式碼的執行順序是由上往下一行一行的執行,同一時間只能做一件事,也就是上一行的程式如果沒有執行完就會卡住下一行程式的執行造成阻塞(blocking),導致後面的程式都無法執行,如果遇到複雜的數學運算或是需要大量CPU運算的任務時就會嚴重影響程式的運作。
非同步(Asynchronous)指的是會先將工作丟出去給其他執行環境(Runtime)處理,不阻塞(non-blocking)後續程式的執行,處理完成後再將結果傳給回調函式(callback function),然後等待Event Loop,例如AJAX或是讀檔寫檔這種存取外部資料的I/O都是常見的非同步行為。
事件迴圈Event Loop的運作流程
了解了以上模組之後,Event Loop也會很好理解,其實就是這些模組一起互動之下形成的無限迴圈,迴圈流程如下
- Javascript engine執行call stack中的任務。
- 當遇到Web APIs或是無法處理的任務時會交給Javascript runtime執行,Javascript runtime處理完成後會將資料交給callback function,並將callback function放進queue中,形成Callback Queue。
- Javascript runtime等待call stack中的任務全部執行結束變成空的,從callback queue中拉取第一個任務放進call stack,回到第一步繼續重複循環下去。
細談Callback Queue
上面說到的Callback Queue是存放callback function地方,也有很多別名例如Event Queue, Task Queue。
通常大部分的文章都只會說到Callback Queue就結束了,但實際上他又可以被細分為Macrotask Queue以及Microtask Queue,差別在於queue中存放的任務有所不同。
Macrotask Queue
macrotask包含:
- 從
<script src="...">
外部下載的script - DOM event handlers,例如mousemove event的callback function handler
- 各種Web APIs,例如setTimeout的callback function
- ajax callback function
macrotask在執行時瀏覽器不會渲染(render)DOM,瀏覽器會被阻塞住,也就是只執行macrotask不會做其他事情,有時我們會看到瀏覽器跳出頁面沒有回應的警告,可能就是因為macrotask有複雜運算(CPU-hungry tasks)或是程式邏輯錯誤導致無限迴圈發生,使得其他任務無法被處理。
Microtask Queue
microtask包含:
- promise
.then/catch/finally
中的 callback function queueMicrotask(func)
中的 func
每個macrotask執行結束後會先將microtask queue中的任務全部執行完,才會繼續執行瀏覽器渲染跟其他macrotask。
將callback queue分解成macrotask以及microtask之後,他們在Event Loop中詳細的運作流程會是這樣:
- 從macrotask queue中拿出一個macrotask丟到call stack中執行。
- 將microtask queue中全部的task依照順序Dequeue到call stack中執行。
- Browser render DOM。
- 如果macrotask queue是空的,sleep直到macrotask再次出現。
- 回到步驟1。
Test
setTimeout(() => alert("timeout"));Promise.resolve()
.then(() => alert("promise"));alert("code");
可以先想想輸出結果,再貼到console中執行看答案是不是跟自己想的一樣,如果一樣的話恭喜你已經了解事件迴圈囉!
如果有任何問題或建議都非常歡迎留言討論,謝謝!
Reference
https://javascript.info/event-loop
https://javascript.info/microtask-queue
https://suprabhasupi.hashnode.dev/how-javascript-works
https://pjchender.blogspot.com/2017/08/javascript-learn-event-loop-stack-queue.html
https://www.youtube.com/watch?v=8aGhZQkoFbQ&ab_channel=JSConf
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management
https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/eventloop.html