學習筆記 - 究竟什麼是 Event Loop?
情境
你正在研究「倒數計時」的功能,在尋找適合 JavaScript 語法的過程中,認識了一個叫 setTimeout 的函式。你快速看過了 MDN 的範例以後,自己隨手做了一點嘗試:
setTimeout(function(){console.log('delay 0 sec')}, 0)
console.log('Hello!')
然後看到了這個結果:
你忽然就很想知道為什麼「延遲 0 秒」的函式明明寫在文件上方,應該先被執行,但在 console 印出的結果,他還是被排在第二順位。
你問某個強者助教這個問題,但剛好助教很忙,沒想太多,他直接丟了一支叫「What the heck is the event loop anyway?」,說這支影片是關於這個問題的經典演講:
Q: 究竟什麼是 Event Loop?
在回答這個問題之前,需要先了解JavaScript runtime的流程,接下來是以影片中提到的Google Chrome browser來解釋。
JavaScript runtime表示能夠執行JavaScript程式碼的環境,其中Google Chrome是以V8 engine來執行JavaScript程式碼,因此Google Chrome是JavaScript的其中一種runtime。
JavaScript 本身是單執行緒(single-threaded)的程式語言,依序處理程式碼,等到當前的程式碼執行完畢之後才會繼續處理後續的程式碼。
那麼JavaScript要怎麼處理非同步(asynchronous)任務呢?
Web APIs: 使JavaScript能夠處理非同步任務
- DOM(document Object Model)
- AJAX(XMLHttpRequest)
- SetTimeOut
- 其他
處理機制介紹
- Call Stack:
依序處理JavaScript程式碼。 - Web APIs:
當函式(function)被推進Call Stack之後,需要非同步處理的函式就會被送到Web APIs處理,讓Call Stack能夠不需等待,直接繼續處理後續的程式碼。 - Task(Callback) Queue:
當Web APIs內的函式處理完之後就會被送到Task Queue並照著先後順序排列。 - Event Loop:
隨時檢查Call Stack 是否已經清空(所有同步程式碼都執行完畢),確認清空狀態後才從Task Queue當中把排列好的任務一次一個送到Call Stack處理。
這邊簡單整理出流程
- 同步任務:
Call Stack — 完成同步任務 - 非同步任務:
Call Stack — Web APIs — Task(Callback) Queue — Event Loop(確認Call Stack清空) — Call Stack — 完成非同步任務
回到問題: 究竟什麼是 Event Loop?
我們有了答案:
當Call Stack處理完同步(synchronous)任務後會清空Call Stack,這時Event Loop 負責將Web APIs處理完成且送到Task Queue等待的非同步(asynchronous)任務回傳(一次一個)到Call Stack接續處理。
其他與JavaScript runtime相關的補充資訊
Call Stack
- Call Stack是追蹤程式碼執行狀態的地方。
- 在Call Stack裡的資料會被指向Memory Heap。
- 遵照後進先出原則(LIFO)。
Stack Overflow
- 當一個函式不斷執行自身這個函式時,會導致Call Stack被填滿以及溢出。
- 當Call Stack溢出,關於Call Stack超過上限的錯誤訊息會出現。
- 下方的程式碼示範一個函式不斷執行自身這個函式,造成Stack Overflow。
function inception() {
inception();
}
inception(); //this causes stack size exceeded error
Memory Heap
- Memory Heap 是分配記憶體存放變數、函式及其他需要用到記憶體的資源的地方。
Garbage Collection
- JavaScript 在資料使用完畢之後會自動釋放該資料所佔用的記憶體。
- 處理memory leaks。
- 自動控制Memory Heap。
- 以Mark and Sweep 演算法來處理Garbage Collection程序。
資料超載時會發生Memory Leaks,例如:array裡面有無窮盡的資料,使browser停止運作。
let array = [];
for(let i=1; i>0; i++) {
array.push(i);
} // this creates an array with infinite data, which causes memory leaks.
JavaScript主要的記憶體管理概念是根據reachability(可到達性),具備"可到達性"的資料就能夠儲存在記憶體,一旦失去"可到達性",該記憶體內的資料就會被清除。
舉例:
// user has a reference to the object
let user = {
name: "John"
};
全域變數user對應參照一個{name: “John”}的物件,這個物件裡的name屬性有儲存一個值"John",這個值可以被取用,具備"可到達性",因此會留存在記憶體。
如果user這個變數的值在後續被覆寫,使參照位址消失。
user = null;
現在{name: “John”}這個物件不具備"可到達性",因沒有任何變數可以指向這個物件,Garbage Collector就會刪除這個資料並釋放記憶體。