iT邦幫忙

2021 iThome 鐵人賽

DAY 8
0
自我挑戰組

登堂入室!前端工程師的觀念技術 _30_ 題系列 第 8

7. 解釋 Event Loop ( 上 ) --- Call Stack

9.9更新: 更正呼叫堆疊的內部為 stack frame。

(提醒:文中的執行環境都是browser(chrome))

今天要解釋JS的Event Loop事件循環,分成上下兩篇,這篇還不會講到Event Loop本身!!

上篇:

  • Call Stack
  • 一點點 Synchronous & Asynchronous 的概念

下篇:

  • Web APIs & Task Queue
  • Event Loop

Call Stack 呼叫堆疊


在之前的文章 JavaScript的執行階段: Execution Context 有提到,在execution phase裡(綠色框框),都是依照 Call Stack的方式執行script。

https://ithelp.ithome.com.tw/upload/images/20210908/20129476ePVRCd5fOV.png

https://ithelp.ithome.com.tw/upload/images/20210908/20129476tFu4W31oif.png
↑execution phase裡(綠色框框)

這邊要先提到Stack的概念:

Stack 堆疊

資料結構是指電腦儲存資料的方式,分成很多種,而「Stack(堆疊)」是一種資料結構。
Stack的特色是 後進先出(Last-in-First-out, LIFO)。

可以想像Stack是一種由下而上堆疊起來的frame(Function calls from a stack of ***frames.***):
https://ithelp.ithome.com.tw/upload/images/20210908/20129476lnHUY79kUD.png

  • 最下面是第一個被放入的frame,然後frame被一個一個堆起來。→ push(黃色箭頭)
  • 如果要把frame抽走,只能從最上方開始拿。→ pop(綠色箭頭)
  • 因此可以知道,越下方是越早放入的frame,越上方則是越晚放入的frame(越新)。
  • 因為最晚放入的,會被最早拿出,所以被稱為"後進先出(Last-in-First-out, LIFO)。"

到這邊應該對Stack比較有概念了,接下來要開始說明Call Stack。

Call Stack


讓我們先看到例子:

function a(){
    function b(){
        function c(){
            console.log('This is c()');
        }
        c();
        console.log('This is b()');
    }
    b();
    console.log('This is a()');
}
a();

然後他的console會是這樣:

This is c()
This is b()
This is a()

在execution phase裡,決定事件處理的順序是這麼做的:

  1. 呼叫a(): 將a放入call stack,
    https://ithelp.ithome.com.tw/upload/images/20210908/20129476wasBnEe0HC.png

  2. 開始讀取function a()裡的程式碼,讀到b(),b也放入call stack。
    https://ithelp.ithome.com.tw/upload/images/20210908/201294765LCNAxvDEj.png

  3. 開始讀取function b()裡的程式碼,讀到c(),c也放入call stack。
    https://ithelp.ithome.com.tw/upload/images/20210908/20129476N5o71DNIcf.png

  4. 執行console.log('This is c()');;c()執行完成,從call stack移除c。
    https://ithelp.ithome.com.tw/upload/images/20210908/20129476NeCqlWUQur.png

  5. 執行console.log('This is b()');;b()執行完成,從call stack移除b。
    https://ithelp.ithome.com.tw/upload/images/20210908/20129476iAqcUPTKDr.png

  6. 執行console.log('This is a()');;a()執行完成,從call stack移除a。
    https://ithelp.ithome.com.tw/upload/images/20210908/20129476gyw7PJSElJ.png

  7. a()執行完畢,call stack被清空。
    https://ithelp.ithome.com.tw/upload/images/20210908/201294767RxqVUDwgP.png

→ 想看動態的可以看這裡,gif版(imgur)

→ 也可以把程式碼丟到這看效果(非常之厲害的視覺化工具)【loupe】

到這邊劃個重點:

Call Stack是在 函式裡呼叫函式 時的一種執行機制。

  • 每調用一次函式,函式都會被放進Call Stack。

  • 只要正在執行的函式裡調用了新的函式,新函式也會被放入Call Stack。

  • 函式執行完成,會被清出,直到call stack被清空。

再讓我們看一個例子:

function f(){
    f();
}

f();

可想而知,執行f()時,call stack可能會長這樣:

https://ithelp.ithome.com.tw/upload/images/20210908/20129476vgjnXkdDxM.png

這種超過Call Stack容量的情況,稱作Stack overflow。

很遺憾的,這個函式會呼叫自己一輩子,這代表我們永遠看不到這個函式執行完的結果。還有電腦可能被燒壞

如果想要解決這個問題,最常見的方法就是使用非同步函式 Asynchronous function。

同步與非同步


Synchronous 同步

當跑一段程式碼(script),瀏覽器(bowser)會將執行結果直接回傳,這樣的執行方式叫做"同步"。

(包含文內的例子(還有至今為止的文章),執行方式都是同步的。)

由於JS是單執行緒(thread),程式碼的執行順序當下同一時間只會處理一件事。所以只要有一段程式碼還沒有執行完成,其他的任務就都會停擺,這樣會造成時間上的浪費。

JavaScript 在傳統意義上是跑在一條單執行緒。即便你的電腦有多顆核心,也只能在 JavaScript 上面跑一條執行緒來完成任務,這一條執行緒我們稱為主執行緒( main thread )。

為了解決這個問題,便有了非同步程式設計。

Asynchronous 非同步

非同步(Asynchronous,又稱異步)則與同步相反,在指定時間或情形,才處理和接收訊息,而非在讀到程式碼時直接執行。

可以看到MDN對非同步的介紹:

Javascript 基本上是一個同步性的、阻塞的,且是跑在單一執行緒的程式語言,也就是在同一時間只能執行一個操作。但是 瀏覽器所定義的函式和 API 允許我們註冊一個不該被同步執行的函式,且這個函式應該在某些事件發生時需要非同步的被呼叫 (到達指定的時間、使用者透過滑鼠互動,或者取得透過從網路所取到的資料)。這代表你可以讓你的程式碼在同時間做一些事情而不需暫停或阻塞你的主執行緒。

如果能將其他任務交給其他處理器來執行,並確認任務何時完成,就可以緩解浪費時間、效率不佳的問題。

那麼下一篇就會講到非同步函式的執行,然後就可以完整解釋Event Loop了。

【如內文有誤還請不吝指教>< 謝謝閱覽至此的各位:D】

參考資料:

-----正文結束-----


上一篇
6. Prototypal inheritance 的運作原理
下一篇
8. 解釋 Event Loop ( 下 ) --- Task Queue ( Callback Queue )
系列文
登堂入室!前端工程師的觀念技術 _30_ 題31

尚未有邦友留言

立即登入留言