[ 筆記 ] JavaScript 進階 02 - Event Loop


Posted by krebikshaw on 2020-09-24

Stack & Queue

要了解 JavaScript 的 Event Loop 之前,要先理解什麼是 Stack?什麼是 Queue?
它們就是所謂的「資料結構」,所謂的資料結構可以理解成某種形式的資料,不同資料結構會有不同類型的特性。比如說常用的陣列 Array 跟 Object 它們也都是「資料結構」,Array 會有 length 長度等等的性質。Object 會有 key & value 相對應的特性。

Stack 堆疊

Stack 可以想像成疊羅漢,先喊到名字的先趴在下面,後喊到名字的就疊上去。當要下來的時候,最上面的人要先下來,下面的人才下的來

老師:「胖虎!」胖虎就先趴在最下面
老師:「小夫!」小夫就疊在胖虎上面
老師:「大雄!」大雄再疊在小夫上面
老師:「可以下來了!」大雄先下來,小夫再下來,最後胖虎才能離開

所以先被呼叫的函式,會先堆在最下面,接下來每呼叫到新的函式,就會一個一個疊上去,並且由「最後被呼叫的函式先執行」,一路執行回來。

function a() {
  return b();
}
function b() {
  return c();
}
function c() {
  console.log('abc');
}
a();

|                    |
| console.log('abc') |  console.log() 最後疊在 c() 上面
|    function c()    |  c() 第三個被呼叫,疊在 b() 上面
|    function b()    |  b() 第二個被呼叫,疊在 a() 上面
|    function a()    |  a() 先被呼叫,堆在最下面
 --------------------

Call Stack
所有的程式語言都有 Call Stack 概念。

當有 a function call b function, 此時 call stack 就會把 a 先丟近來、再把 b 丟在 a 上方。

當執行過程出現錯誤的時候,我們在錯誤訊息中,也可以看到系統印出錯誤的順序,就是遵循 Stack 的流程

當我們寫了一個無窮堆疊的函式(不斷自己呼叫自己)

瀏覽器會因為 Call Stack 被填滿、出現錯誤,很可能會拋出 Maximum call stack size exceeded,也就是鼎鼎大名的 Stack Overflow。

Queue 佇列

Queue 可以想像成排隊,你越早來排隊,就會越早輪到你

老師:「小夫!」小夫排在第一個
老師:「大雄!」大雄排在第二個
老師:「胖虎!」胖虎排最後一個
老師:「可以來拿點心了!」胖虎把大雄跟小夫推到後面,自己當第一個!
(啊!舉錯例子了,排隊的概念不適用於胖虎~)

Callback Queue
當 JavaScript 執行到類似 setTimeout 或者 EventListener 這類型「非同步」的 callback function 時,瀏覽器會先將它放在一個 web API 去執行,等到它執行結束後,就會把結果的 Callback function 傳進去 Callback Queue 裡面排隊。

|         stack        |              |      web api       |     
|                      |              |                    |
|                      |              |                    |
|                      |              |                    |
|   console.log('abc') |              |                    |
|     function c()     |              |                    |
|     function b()     |              |                    |
|     function a()     |              |   callback func()  |
 ----------------------                --------------------
有分層結構的任務丟進 stack                非同步的任務丟進 web api
                                      執行完之後放進 Queue 裡排隊        

                                      |       Queue        |
                                      |                    |
                                      |                    |
                                      |                    |
                                      |                    |
                                      |                    |
                                      |   callback func()  |
                                       --------------------

在 Callback Queue 裡面排隊的這些 callback function 會等到 Call Stack 都清空了以後,才會被放到 stack 去執行。

所以非同步的執行原理,就是讓 callback function 在 web api 執行完再到 Queue 排隊等待。如此一來就不會把 stack 給 block 住了。

Event Loop 事件迴圈

瀏覽器在跑 JavaScript 是 single thread (單執行緒),一次只能執行一個任務,所以要有一個機制來跑非同步的東西,而機制的其中一個環節就叫 Event Loop,等於是決定執行任務順序的主宰者。

可以想像成 JavaScript 只能跑 single thread,但瀏覽器可以跑 multiple thread,所以會利用 Event Loop 機制去幫助 JavaScript 執行任務。

Event Loop 做的事情就是「 不斷得去監測 Call Stack & Callback Queue 」

Event Loop 的監測順序:

  1. 看 Call Stack 有沒有東西,有就執行
  2. 看 Callback Queue
    • 如果 Call Stack 為空,且 Callback Queue 有東西,就將 Callback Queue 的東西移到 Call Stack
  3. 回到步驟 (1.)

第一優先順序永遠都是 Call Stack,所以有一個問題是,如果 Call Stack 一直有任務的話,那 Callback Queue 就不會被執行到。

所以依照這個邏輯,如果 Stack 卡住了,Queue 就塞在那邊永遠動不了,這就是為什麼每次網頁卡住時,我們發現某個按鈕沒有反應,重複去點了好幾次之後,等到網頁跑好的瞬間,剛剛點選的功能會爆出來超級多次。原因就是因為 Stack 卡住之後,我們點選按鈕時產生的 callback function 其實都在 web api 裡執行完通通塞進 Queue 裡面等待了。

觀念釐清(下文擷取自老師的檢討文章)

原文網址:https://github.com/Lidemy/mentor-program-4th/tree/master/examples/week16

以下面的程式碼為例:

setTimeout(() => {
  console.log('hello')
}, 0)

執行流程是什麼?

是先把這整段放到 call stack 裡面去執行,所以才會執行 setTimeout 這個 function。然後 setTimeout 會呼叫瀏覽器幫忙設定一個 0 ms 後到期的定時器,到期之後就會把第一個參數:() => {console.log('hello')} 放進去 callback queue。

這邊最多人誤解的點就是會把 setTimeout(...) 整段丟進去 callback queue,不是這樣的,只會把第一個參數丟進去而已。你必須先執行 setTimeout 才能設定計時器,才能把第一個參數丟進去 callback queue。

然後還有另一個會搞錯的地方,那就是很多人以為是把 console.log('hello') 丟進去 callback queue,不是,這是一個 function call,不是一個 function。丟進去 callback queue 的是 () => {console.log('hello')} 這個 function。

設定完成以後從 call stack pop 出來,main 也 pop,stack 清空,把 () => {console.log('hello')} 丟進去 call stack,執行這個 function,執行之後發現這個 function 裡面還要呼叫 console.log('hello'),所以把 console.log 丟進去 call stack,印出 hello,pop,然後原本的 function 也沒東西要執行了所以也 pop,stack 清空,結束。

錯誤範例

底下找幾個現成的錯誤範例來解釋錯在哪裡:

範例一

console.log(1)        // 放入 Call Stack 並直接執行,印出 1,執行完後移除
setTimeout(() => {    // setTimeout() 放到 Webapis 執行,直到倒數完畢,
  console.log(2)      // () => { console.log(2) } 被放到 Callback Queue 待命
}, 0)

錯誤的點在:「setTimeout() 放到 Webapis 執行」,web api 不是一個地方,是一個種類,setTimeout 是屬於 web api 的其中一個,但是不是 web api 跟非同步無關。

範例二

  1. console.log(1)放入 call stack 執行,輸出 1
  2. setTimeout(() => { console.log(2) }, 0)放入 call stack 執行,在經過 0 秒後呼叫() => { console.log(2) },由於 setTimeout 屬於 WebAPI,
    所以將 () => { console.log(2) } 排進 callback queue,執行結束後,setTimeout 就會從 call stack 中 pop 掉

錯誤的點在於第二步,「由於 setTimeout 屬於 WebAPI,所以...」,不是,這跟是不是 WebAPI 無關,而是跟 setTimeout 本身要做的事有關。

換句話說,有同步的 WebAPI,也有非同步的 API,有同步的不是 WebAPI 的東西,也有非同步的不是 WebAPI 的東西。

這邊我看了一下我之前寫的文章,發現是我讓大家誤解了,描述得不夠好,我文章中是這樣寫的:

然後 setTimeout 屬於 Web API,所以會跟瀏覽器說:「欸欸,幫我設定一個計時器,2000 毫秒以後呼叫 fn」,然後就執行結束,從 call stack 裡面 pop 掉。

我這邊要強調的是:「因為 setTimeout 是 WebAPI,所以會跟瀏覽器溝通,要瀏覽器去執行某些事情」,而不是「因為是 WebAPI 所以會把 callback 丟進 callback queue」。

範例三

setTimeout 放進 call stack
因為 setTimeout 是非同步函式,所以會移進 Web API 等待時間到
經過 0 ms 之後,將 () => { console.log(2)} 放進 Queue
此時因為 call stack 裡也有任務正在執行,所以先在 Queue 裡面等待

錯誤的點在於 Web API 不是一個地方。這邊可以直接講瀏覽器就好,呼叫 setTimeout 之後叫瀏覽器設定一個計時器,0ms 之後會觸發,那這個計時器設定在哪邊?不重要,這是瀏覽器會去處理的事。

範例四

setTimeout(() => { console.log(2) }, 0) 放進 Call Stack 執行,呼叫 setTimeout 這個 Web API,本行執行完畢,setTimeout 倒數時間設定為 0,倒數完畢後,將 console.log(2) 放入 Callback Queue 待命。

錯誤的點在於:將 console.log(2) 放入 Callback Queue 待命,是將 () => { console.log(2) } 放入 callback queue。

有關 setTimeout 的延伸閱讀:

  1. 为什么 setTimeout 有最小时延 4ms ?
  2. HTML spec: 8.6 Timers

#Event Loop







Related Posts

Day03:從迴圈看 bytecode

Day03:從迴圈看 bytecode

Day 0:7 天開發 Web 版瑪利歐 系列文簡介

Day 0:7 天開發 Web 版瑪利歐 系列文簡介

Python Table Manner - 程式碼風格

Python Table Manner - 程式碼風格


Comments