六角學院 JS 核心筆記 (九)【執行環境與作用域】- 執行緒與同步、非同步
前言
事隔好幾個月,才又開始更新,因為這個章節的觀念非常龐大且複雜,看完影片和參考資料後,根本不想做筆記,時間全部拿去玩動森啦!!!
如果對執行緒的基本概念不太了解的話,可以參考電腦的核心 (Core) 和執行緒 (Thread)。
這邊還有兩個我很討厭的名詞:同步 (Synchronous) 和非同步 (Asynchronous)。因為中文的翻譯超容易誤解。同步有循序、依序的意思。非同步有同時的意思。
單執行緒 (Single Thread)
所謂的單執行緒,就是只有一個 call stack,所以一次只能做一件事情。我必須先刷牙,刷完牙才去吃早餐,吃完早餐才洗盤子。不能一心二用。系統上在執行 JS 語言時採用單執行緒。
同步 (Synchronous、Sync)
顧名思義,同步是按順序執行的意思,也就是程式碼的每個 statement 都被一條一條地依序執行。
As the name suggests synchronous means to be in a sequence, i.e. every statement of the code gets executed one by one. So, basically a statement has to wait for the earlier statement to get executed.
但是缺點是,如果今天讀取檔案 (例:背景圖片) 的程式碼在最前面,它是採用 Synchronous 而且花費超久的時間,那麼打開網頁的使用者,可能等了很久頁面還是一片空白,因為處理事件的流程會被「卡住」了,後面顯示文字的程式碼無法執行。
參考資料:
GeeksforGeeks:Synchronous and Asynchronous in JavaScript
非同步 (Asynchronous、Async)
非同步就是,不需要等前一件事情做完,就能同時處理其他的事,
如此一來,處理事件的流程不會被「卡住」。
例如:使用者打開一個網頁時,如果 JS 沒有「非同步」的特性,可能會跑完兩行字,開始去其他地方拿資源,例如:一張圖片,當圖片沒有全部被載入,圖片後面的文字也無法出現,這通常不是使用者能接受的。如果 JS 有「非同步」的特性,那麼網頁中的文字會完全顯示出來,等圖片全部載入後,圖片再顯示出來,在圖片出現之前,使用者能閱讀網頁中的所有文字內容,這才是我們要的。
既然 JS 有「非同步」的特性,不就是可以同時處理很多事嗎?不算是多執行緒嗎?
不是的,執行緒主要是系統看待程式的方法,JS 只在一個核心或一個模擬核心上運行,只有一個 call stack。而同步 (循序)、非同步 (平行) 是執行緒完成任務的方法,可以想成是它如何安排不同函式之間的運作順序,並且順利地完成整個任務。
JavaScript 是一個同步的單執行緒語言,但是它會用額外的方式處理非同步事件。
怎麼處理呢?利用事件佇列 (Event queue)。
事件佇列 (Event Queue)
Queue
Queue 中文叫做佇列,是一種很基本的資料結構型態,通常會和 Stack 一起被討論。Queue 是線性資料結構,遵循特定的執行順序,也就是先進先出或後進後出。
A Queue is a linear structure which follows a particular order in which the operations are performed. The order is First In First Out (FIFO).
概念像是排隊買電影票,先排的人先買完票離開。
參考資料:
GeeksforGeeks:Queue Data Structur
事件佇列
當 JS 判斷現在遇到一個非同步的事件,例如:setTimeout
、DOM (document) 包含各種監聽事件
、ajax (XMLHttpRequest)
和 fetch
等,也就是非 JS 本身擁有的函式,會先將該事件從原本的 main thread 剝離。
這邊外插一下,所謂的非 JS 本身擁有的函式指的是「JS 執行環境提供的函式」。
- 當瀏覽器開啟網頁 (即用瀏覽器跑 JS),此時 JS 執行環境就是瀏覽器,瀏覽器會提供
setTimeout
、DOM (document) 包含各種監聽事件
、ajax (XMLHttpRequest)
和fetch
等函式給 JS 使用。 - 當用 Node.js 啟動 JS,此時 JS 執行環境就是 Node.js,Node.js 會提供
setTimeout
、os
、http
、path
和fs (File system)
等函式給 JS 使用。
於是當程式碼判斷,現在要呼叫一個「執行環境提供的函式」時,會先將該事件丟給瀏覽器或 Node.js,執行環境會開另外一個 thread 去計時。
當任務執行完畢後,將需要回傳的任務 (函式),丟到事件佇列 (Event Queue) 中,又稱 task queue 或 callback queue。
Event Loop 函式會一直去判斷 call stack 是不是被清空了?當 call stack 中一旦被清空了 (其他同步事件都完成後),才會將在事件佇列中排隊的函式們,依序抓回 main thread 的 call stack 中,並且執行。
圖片來源:
Understanding Event Loop, Call Stack, Event & Job Queue in Javascript
舉例
下面的例子中,不管 setTimeout
設置的時間是 0 還是 1 秒,也不管先呼叫 a()
還是先呼叫 b()
,a
永遠會在 b
之後被輸出。
1 | function a() { |
因為在 JS 的 call stack 中,a()
先出現,發現有 setTimeout
,setTimeout
計時器被丟到瀏覽器,開始計時 0 秒,一旦計時結束,setTimeout
就會被丟到事件佇列中,計時開始的同時,a()
結束,b()
出現,main thread 中執行 console.log('b');
,於是先輸出 b
。
main thread 中 console.log('b');
結束,b()
結束,call stack 終於被清空了後,事件佇列中的 setTimeout
被丟回 main thread 的 call stack 中,執行內部的 console.log('a');
,於是 a
永遠在 b
之後出現。
下面這個影片解釋的很詳細,有中文字幕。
(應該是) 演講者還寫了一個可以觀察函式在 call stack、瀏覽器和事件佇列中如何移動的網頁,很有趣,大家可以玩玩看:
loupe (他預設的程式碼)
我上面的例子放在 loupe 網頁中
參考資料:
huli 大大的文章 - JavaScript 中的同步與非同步(上):先成為 callback 大師吧!
六角學院:JavaScript 核心篇 - 邁向達人之路