理解 Event Loop 1 - 先談 Execution Context 和 Call Stack
前言
JavaScript 是 單執行緒 的語言,意思是每次只做一件事。但是在網頁許多互動功能,都會關係到「非同步」行為,例如:呼叫 API、setTimeOut
計時,若執行太久會形成 blocking ,後續的程序都被卡住不能執行,導致使用體驗不佳
Event Loop 就是要解決這樣的問題,調和「同步」與「非同步」
白話來說,會優先處理 同步 的程式碼,非同步 則轉移瀏覽器 Web API 處理,等到 同步 的部分都已完成,再來繼續收尾 非同步 的程序
以實際運作機制來看,event loop 是一個司令,不斷檢查 call stack
是不是空的,若已被清空,再來開始 event queue
的東西塞到 stack
但談到 call stack
,又要談到 execution context
了
先談 Execution Context ( 執行環境 )
execution context 的主要類型 :
- global execution context (每個 JavaScript 程式檔只會有一個)
- functional execution context (執行函數時會建立)
其實 execution context 像是沙盒一般,每個 context 每個都有獨立自主的環境
其實還有 eval 內的 execution context,但先略過,不建議使用,關鍵字搜尋
eval is evil
Global Execution Context
一開始瀏覽器執行 JavaScript 時,首要的預設執行環境,其中又分成兩階段
- 創造階段
- 執行階段
創造階段 global execution context 創立時會做三件事
- 創造全域環境,也就是全域物件
window
- 創造一個
this
變數,指向window
- 進行記憶體指派,先將變數和 function 分配至記憶體,這也是 hoisting 的來由
- 變數 : 宣告的變數會先預先指派記憶體,但不會被 賦值 ,此時值為
undefined
- 變數 : 宣告的變數會先預先指派記憶體,但不會被 賦值 ,此時值為
執行階段 如人類直覺,程式碼會由上而下,逐行執行
- 對變數進行賦值
- 碰到 function call,則暫時停止 global 執行階段,新增 functional execution context,並把該 Context 加入 execution stack
Functional Execution Context
當函數被調用時,就開始 functional execution context,也一樣分成 創造階段、執行階段
執行階段 還是一樣,程式碼會由上而下,逐行執行
- 對變數進行賦值
- 裡面又碰到 function call,則暫時停止執行,新增 functional execution context,並把該 context 加入 execution stack
Execution Stack (Call Stack)
stack 是一種 先進後出 (LIFO) 的 資料結構, call Stack 則 以這種資料結構來制定任務的執行順序
什麼是先進後出呢?可以想像:把品克洋芋片一片一片裝進罐子,裝滿之後,第一片會在最下面,可是開始吃的時候是從最後一片,也就是最上面那一片開始吃
或是,可以想像每一個 execution context 就是一張待辦清單便條紙,這個過程是 while loop
- 碰到 function call 就是把一張新的便條紙覆蓋上去,待辦事項就是 function 內的程式碼
- 開始 依序執行 便條紙內的待辦事項
- 如果待辦事項沒有 function call,沒事了,這個 function 執行完畢就可以把便條紙撕掉
- 但如果又碰到 function call,就暫停,貼上一張新的便條紙
- ( 持續檢查最上面一層有沒有便條紙,如果有,就繼續 依序執行 裡面的待辦事項 )
call stack 是理解 event loop 的階梯之一,這個部分需要視覺化跟舉例才會比較清楚
我們以下面程式碼來理解 call stack
function openBox1() {
console.log("這裡是盡頭了");
}
function openBox2() {
openBox1();
console.log("Box2 解析完畢");
}
function openBox3() {
openBox2();
console.log("Box3 解析完畢");
}
openBox3();
// 印出的順序為 "這裡是盡頭了"、"Box2 解析完畢"、"Box3 解析完畢"
- 開始進入主程式,此時 stack 為
[main]
(global execution context) - 開始執行
openBox3
,進入函數- 此時 stack 為
[main, openBox3]
- 此時 stack 為
- inside
openBox3
- 開始執行,碰到需調用
openBox2()
,暫停並進入openBox2()
函數 - 此時 stack 為
[main, openBox3, openBox2]
- 開始執行,碰到需調用
- inside
openBox2
- 開始執行,碰到需調用
openBox1()
,暫停並進入openBox1()
函數 - 此時 stack 為
[main, openBox3, openBox2, openBox1]
- 開始執行,碰到需調用
- inside
openBox1
- 碰到
console.log("這裡是盡頭了")
,加入 Stack - 此時 stack 為
[main, openBox3, openBox2, openBox1, console.log("這裡是盡頭了")]
console.log
執行完畢,pop off,此時為[main, openBox3, openBox2, openBox1]
- 函數內容全數執行完畢,pop off
openBox1
,此時為[main, openBox3, openBox2]
- 碰到
- 重新回到
openBox2
- 碰到
console.log("Box2 解析完畢")
,加入 stack - 此時為
[main, openBox3, openBox2, console.log("Box2 解析完畢")]
console.log
執行完畢,pop off,此時為[main, openBox3, openBox2]
- 函數內容全數執行完畢,pop off
openBox2
,此時為[main, openBox3]
- 碰到
- 重新回到
openBox3
- 碰到
console.log("Box3 解析完畢")
,加入 stack - 此時為
[main, openBox3, console.log("Box1 解析完畢")]
console.log
開始印出,執行完畢,把該任務 pop off,此時為[main, openBox3]
- 函數內容全數執行完畢,pop off
openBox3
,此時為[main]
- 主程式全部執行完畢了,清空,stack 為
[]
- 碰到
文字容易頭昏眼花,可以直接進入這個 視覺化網站
總結一下
- 不考慮 hoisting 的話,程式碼都是由上而下執行,但如果碰到 function call,就需要探討 call stack
- 在 call stack 當中要注意的是,global execution context 會是第一張待辦清單便條紙,若碰到 function call 就會新增一張待辦清單便條紙,並且疊上去 stack,依序執行待辦清單
- 「執行時碰到 function call」 其實就是在 call stack 新增一張 function execution context 便條紙疊上去
- 不斷檢查是否還有便條紙,若有,就從最上面那張依序執行,直到所有便條紙都被清空