--- title: Understanding Event Loop tags: notes description: MFEE16 Node.js Homework version: 20210525 --- ## 筆記:What the heck is the event loop anyway? -- Philip Robert ###### keywords: `call stack` `job queue` `event loop` `concurrency` `setTimeout` `asynchronous` `single-threaded` `blocking` ###### video: https://youtu.be/8aGhZQkoFbQ ## the call stack > one thread == one call stack == one thing at a time ### 單執行緒 single thread JavaScript是一種單執行緒的程式語言(single-threaded programming language),代表一次只能執行一段程式碼(run one piece of code at a time),並且會在堆疊(stack)中紀錄目前執行到哪一段程式碼(records where we are in the program)。 ### 呼叫堆疊 call stack 堆疊(stack)是一種後進先出(LIFO, Last In First Out)/先進後出(FILO, First In Last Out)的資料結構(data structure): * 當進入某一個函式,這個函式會被推至堆疊的最上方 `push` (if we step into a function, we put someting on to the stack) * 當該函式中執行結束,則會將此函式從堆疊的最上方抽離 `pop` (if we return from a function, we pop off the top of the stack) ```javascript= [code] // [stack] function multiply(a, b){ // 3. push return a * b; // 4. pop } function square(n){ // 2. push return multiply(n, n); // 5. pop } function printSquare(n){ var squared = square(n); console.log(squared); // 6. push } printSquare(4); // 1. push 7. pop ``` 以上述程式碼為例,在執行JavaScript的函式時,首先進入堆疊的會是此檔案中全域環境的程式(講者稱作`main()`),`printSquare()` 才會被呼叫進入堆疊最上方,接續在 `printSquare` 中呼叫 `square()` 進入堆疊的最上方,以此類推,同樣在 `square` 中呼叫 `multiply()` 時, `multiply` 進入堆疊最上方。 當執行到每一個函式中的`return`或結束時,該函式便會抽離堆疊。 * a function baz which calls bar which calls foo which throws an error: ```javascript= function foo() { // 2 throw new Error('Oops!'); // 1 } function bar() { // 3 foo(); } function baz() { // 4 bar(); } baz(); // 5 ``` ``` Uncaught Error: Oops! foo bar baz (annoynmous function) ``` ### 無窮迴圈 infinite loop 講者以下方範例示範無窮迴圈,程式碼會不斷被疊加上去直到瀏覽器出現錯誤: * blowing the stack ```javascript= function foo(){ return foo(); } foo(); ``` ## blocking > what happens when things are slow? ### 阻塞 blocking 當執行程式碼片段需要等待很長一段回應時間,或出現類似畫面被「卡住」的現象,稱作為阻塞(blocking)。 * blocking -- code that is slow * 速度比較:console.log > while loop > image/web request ## why is this a problem? > because, browsers. 這也是AJAX是以非同步執行(asynchronous)處理的原因。假設AJAX Request變為同步(synchronous)處理的話,等於每發出一次請求,就必須等待函式執行完後才能繼續往下走,也就是說等待回應的過程不能進行其他動作,講者以虛擬碼(pseudo code)進行說明: ```javascript= var foo = $.getSync('//foo.com'); var bar = $.getSync('//bar.com'); var qux = $.getSync('//qux.com'); console.log(foo); console.log(bar); console.log(qux); ``` 當堆疊中有未處理完的函式導致阻塞產生時,就沒辦法在瀏覽器執行其他任何動作,瀏覽器也無法重新渲染(render),必須等到request執行結束後瀏覽器才會繼續運作。可知堆疊被阻塞(stack blocked)的情況會導致瀏覽器無法繼續渲染頁面,導致頁面「停滯」。 上述的情況並不理想,因此若希望看到流暢的UI,就不能阻塞stack。 ## the solution? asynchronous callbacks > here's a function. call me maybe? 為了避免阻塞問題,瀏覽器/Node的設計是透過非同步(asynchronous)的方式搭配 callback 來執行程式碼 ,講者以 `setTimeout` 模擬非同步請求的進行,並以下面程式碼為例: ```javascript= console.log('hi'); setTimeout(function cb() { console.log('there'); }, 5000); console.log('JSConfEU'); ``` ``` hi JSConfEU there ``` 在執行這段程式時,執行堆疊(call stack)中會先打印 `hi` ,接著執行`setTimeout` (設定5秒鐘倒數計時),但是在 `setTimeout` 中的這個回呼函式(call back function, cb)並不會立即被執行,而是接下去打印出 `JSConfEU` ,(五秒倒數完畢)最後才執行打印出 `there` 。這是怎麼發生的?這就需要提到 ==事件循環(event loop)== 在並行執行(concurrency runtime)發揮作用的地方了。 ## Concurrency & Event Loop > one thing at a time, except not really. 首先,JavaScript runtime一次的確只能做一件事,但瀏覽器並不只有一個JavaScript Rutime,還提供不同的Web API直接使用,因此可以同時處理多項事件(do things concurrently)。 ![img](https://i.imgur.com/9brnpzB.png) :::info **setTimeout()** -- extra stuff got from browser which sits in WebAPIs ::: 透過上方的圖解可以了解到 `setTimeout` 是一個瀏覽器提供的 WebAPI ,並不存在於V8引擎。當在堆疊(stack)中執行 `setTimeout()` 時,瀏覽器會開啟一個倒數計時器,將 `setTimeout` 中的回呼函式(callback function, cb) 放到 WebAPIs 中,此時 `setTimeout()` 便執行結束,並從堆疊中抽離。 ![setTimeout](https://i.imgur.com/IAqNrhI.png) 當計時器的時間一到,會把要執行的 cb 放到工作佇列(task queue)。 ![task queue](https://i.imgur.com/oFeUyeA.png) 這時候就輪到事件循環(event loop)發揮作用 --- 如果堆疊是**空的**,它便把佇列中的第一個項目放到堆疊當中執行。 ![event loop](https://i.imgur.com/w2wvr0b.png) :::info **event loop** - job is to look at the call stack and look at the task queue, if the stack is empty it pushes the first thing on the queue to the stack and effectively runs it ::: ### setTimeout 0 同理,即使將倒數時間設定為0秒鐘,結果不變,因為條件是必須等到堆疊清空後,再「立即」執行。 如同範例而言,將 cb 放入 WebAPIs 的計時器中,當時間倒數完畢,該 cb 會被放入工作序列內,等到堆疊清空後才會放置堆疊中執行 cb ,因此打印的順序ㄧ樣為 `hi` --> `JSConfEU` --> `there`。 ```javascript= [code] console.log('hi'); setTimeout(function cb() { console.log('there'); }, 0); console.log('JSConfEU'); ``` ``` [result] hi JSConfEU there ``` ### AJAX Request 將 `setTimeout` 模擬非同步處理的程式碼,套在處理 AJAX 請求也是一樣的道理。如同 `setTimeout` ,執行 AJAX 請求的程式碼不在V8引擎中執行,而在 WebAPIs 內。 ![img](https://i.imgur.com/79VBWVs.png) :::info **[XMLHttpRequest (XHR)](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)** - an **API** in the form of an object (provided by the browser's Javascript environment) whose methods transfer data between a **web browser** and a **web server**. - enables a Web page to update just part of a page without disrupting what the user is doing which is used heavily in **AJAX** programming. - can be used with protocols other than HTTP and data can be in the form of not only XML, but also JSON, HTML or plain text. ::: 當執行 AJAX 請求時, cb 的內容會被放置 WebAPIs 中,原本的堆疊則繼續往下執行。直到 AJAX 請求給予回應後,不論成功或失敗,皆會把 cb 放到工作佇列當中。當堆疊被清空時,事件循環會再將 cb 推進堆疊中執行。 ```javascript= [code] console.log('hi'); $.get('url', function cb(data){ console.log(data); }); console.log('JSConfEU'); ``` ``` [result] hi JSConfEU { "some": "json" } ``` 為了方便理解 JavaScript 中 call stack / event loop / task queue 之間的關係, 講者提供自己將程式碼視覺化的工具 [Loupe](http://latentflip.com/loupe) 及以下可以貼到網站中執行的不同範例: ### Click Event ```javascript= console.log('Started') $.on('button', 'click', function onClick () { console.log('Clicked') }) setTimeout(function onTimeout () { console.log('Timeout Finished') }, 5000) console.log('Done') ``` 1. `console.log('Started')` 會先被放到堆疊中執行,印出 `Started`。 2. 由於 `click` 和 `setTimeout` 都屬於 WebAPIs,當被放入 WebAPIs 後即從堆疊中脫離後,`console.log('Done')` 被放置堆疊中執行,印出 `Done` 。 3. `setTimeout` 計時器的時間一到,或 `click` 事件被觸發時, WebAPIs 會將 cb 放到工作佇列中。 4. 當堆疊清空時,event loop 會將工作佇列中搬到堆疊中執行(印出`Clicked` / `Timeout Finished`)。 因此瀏覽器執行點擊時,點擊事件的 cb 並不會立即被執行,而是先被放到工作佇列中,直到堆疊空出才會被執行。 ### Multiple setTimeout ```javascript= setTimeout(function timeout() { console.log('hi') }, 1000) setTimeout(function timeout() { console.log('hi') }, 1000) setTimeout(function timeout() { console.log('hi') }, 1000) setTimeout(function timeout() { console.log('hi') }, 1000) ``` 從這段程式碼,可以了解到 `setTimeout` 的計時器不保證會在設定的毫秒內「立即」執行,毫秒數應看作是執行的最少時間(minimum time)。如同 `setTimeout` 0秒,不會「即刻」執行,而是接下來某個時間才執行。 參考資料: [【JavaScript筆記】所以事件循環Event Loop到底是什麼?setTimeout 0 的藝術 ─ 我OK、你先請?](https://emilywalkdone.blogspot.com/2021/01/JavaScript-EVENT-LOOP.html) [[筆記] 理解 JavaScript 中的事件循環、堆疊、佇列和併發模式(Learn event loop, stack, queue, and concurrency mode of JavaScript in depth) ](https://pjchender.blogspot.com/2017/08/javascript-learn-event-loop-stack-queue.html)