JavaScript中,並行模型(concurrency model)建立於事件迴圈(event loop)的基礎上。Event loop的功能在於執行程式碼、蒐集與處理事件、以及執行等待中的次任務(sub-tasks).
JavaScript是一種單線執行(single threaded)的程式語言,亦即一次只有一條線執行所有的事。例如,有一個for
迴圈需要花一段時間執行,則必須等待這個for
迴圈執行完畢,才能繼續執行後面的程式碼,這就會造成阻塞(blocking)。
舉例來說,執行以下程式碼:
for
迴圈跑了999999999個迴圈,造成阻塞,因此有一段時間停滯。並且因為console.log()
是 I/O 操作,會佔用大量記憶體,甚至可能會拖垮開發工具。關於JavaScript如何執行非同步事件,這篇參考文章有詳細的說明:談談JavaScript中的asynchronous和event queueImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
重複一下上面的範例,這次使用setTimeout()
這個方法:
setTimeout()
,開始計時2秒setTimeout()
計時2秒結束,印出"the result?"JavaScript中的事件循環(Event Loop)
簡單來說,事件循環是一個用於管理code執行的系統,我們**想用JS非同步(asynchronously)**執行不阻塞的程式碼,這就是事件循環發揮功能的地方了。事件循環的主要元素包含:
以無順序的方式儲存物件的記憶體。當前使用的JavaScript變數和物件等會儲存在heap中。
前面提到,JavaScript是以單線的方式執行,所有待執行的程式碼片段會被放在堆疊(call stack)中執行。當調用函式時,一個幀(frame)會被加入堆疊中,幀連結了該函數的參數(argument)與heap中的變數。Frame是以後進先出(Last in first out,LIFO)的順序進入堆疊中,可以想成array.push()
和array.pop()
:
array.push()
將元素加在array的最後array.pop()
將最後一個元素移除,並回傳被移除的元素例如以下程式碼:
console.log(one()()())
其實是:所以,函式執行的順序是: 1️⃣ one()
2️⃣ two()
3️⃣ three()
console.log(one()()())
; 時,Call Stack 變化如下:1️⃣ console.log()
進入 Call Stack(但要等 one()()()
執行完才輸出)
2️⃣ one()
進入 Call Stack
3️⃣ one()
回傳 two,從 Call Stack 彈出
4️⃣ two()
進入 Call Stack
5️⃣ two()
回傳 three,從 Call Stack 彈出
6️⃣ three()
進入 Call Stack
7️⃣ three()
回傳 'Done!',從 Call Stack 彈出
8️⃣ console.log('Done!')
執行並輸出 'Done!',然後從 Call Stack 彈出
最終執行順序
函式執行順序(進入 Call Stack 順序): 1️⃣ one()
2️⃣ two()
3️⃣ three()
函式結束順序(從 Call Stack 彈出順序): 1️⃣ three()
2️⃣ two()
3️⃣ one()
4️⃣ console.log()
(最後輸出 'Done!')
總結:
進入Call Stack的先後順序:
(底部–>頂部)
global()
console.log(function)
one()
two()
three()
在JavaScript執行程式碼時,會由上而下、優先從全域(global)的程式碼開始執行,若全域的程式碼需要進入某個函式,再執行該函式,並把此函式加入stack的最上方,接著執行到函式中的return,函式便會移出堆疊(pop off),以下程式碼舉例:
由上而下、先執行全域的console.log('hi')
(印出hi),接著執行setTimeout
函式(開始計時五秒),接著執行console.log('JSConfEU')
(印出JSConfEU),接著五秒倒數歸零,印出there。
非同步程式碼(如 setTimeout, Promise 等)在執行時會將回調函式(callback function)放入 Callback Queue。
當非同步操作完成後,回調函式並不會立即執行,而是進入 Callback Queue 等待執行。
前文中的例子顯示,在瀏覽器中可以同時處理多個事情,因為瀏覽器不是只有一個JavaScript Runtime(執行環境),如前文中的setTimeout
,是瀏覽器提供的一個API(Web API),而非JavaScript engine本身的功能。
關於執行環境(runtime),這篇參考文章中有更詳細的說明:從「為什麼不能用這個函式」談執行環境(runtime)Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Web API: 如DOM、ajax、setTimeout、HTTP Request等等,可以幫助單線處理的JavaScript在瀏覽器中完成更多事。Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
更多WebAPIs可參考MDN文件。
setTimeout
中的 callback function(簡稱 cb)會被放到 WebAPIs 中,這時候,setTimeout
已經執行結束,並從call stack中脫離。當計時時間到,就會將cb放入佇列(queue)中。queue是以先進先出(First in first out,FIFO)的方式執行,等待call stack中的任務清空,由事件循環監控,將queue中的任務傳入stack中依序執行。
因此,簡單的說,queue就像此單線的to-do-list,存放等待被執行的事件,因此,重複檢查queue中是否有任何需要被執行的事件,若有,則待call stack清空後,將佇列中的事件傳入call stack,直到queue為空;這樣重複監控call stack與queue的循環,稱為事件循環(Event Loop)。
輸出順序:
回到前文中的範例程式碼,假如把程式碼中setTimeout等候時間改為0,瀏覽器同樣先執行console.log('hi')
,遇到setTimeout時間為0,將setTimeout的cb放入WebAPI的計時器中,等時間到再把此cb放入佇列(queue),然而堆疊中任務尚未執行完畢,因此要等console.log('JSConfEU')
也執行完畢,最後才會印出there。
setTimeout 的等待時間非執行的保證時間,而是要求執行環境處理所需的最少等待時間 (因此,不管設定時間是0秒或5秒,都必須等待call stack清空才可以將queue中的cb放入call stack執行)。
如MDN文件中的例子:
首先,console.log("First line")
被加入call stack並執行,印出'First line',接著被移出call stack
setTimeout()
被加入call stack
setTimeout()
是非同步的,執行後,它會設定計時器,當計時器倒數至 0 後,setTimeout
函式本身會從call stack中彈出。
Note:計時期間,
usingsetTimeout
並未進入Event Queue,而是計時器歸零後,usingsetTimeout
才會被放入Event Queue等待執行。
同時,事件循環也不斷檢查call stack是否都被執行完畢,當 call stack 為空時,它才會從 Event Queue 取出callback函式並執行。
console.log("Last line")
被加入call stack並執行,印出'Last line',接著被移出call stack
等待 3000 毫秒:
在等待期間,setTimeout
的callback函式,也就是usingsetTimeout
並未進入Event Queue中,它只是在等待計時器倒數結束。
3000 毫秒後,計時器歸零,usingsetTimeout
被放入Event Queue,等待執行。
事件循環發現 call stack現在空了,因此把Event Queue中排在最前面的usingsetTimeout
推進call stack。 Event Queue 進入 call stack 的函式,仍然按照「先進先出(FIFO)」的順序執行。
usingsetTimeout
執行後,console.log("queue")
被加入call stack並執行,印出'queue',然後被移出call stack(遵守call stack後進先出LIFO的順序)。
最後,usingsetTimeout
被移出call stack。