[筆記]前端開發內功心法之瀏覽器事件循環
編寫紀錄(History)
2023.11.12 初版(寫到一半遇到編輯器Bug功能沒反應,重開後整個文件都沒存檔只好重頭來過…)
0x00 前言
Web開發入門說簡單也是很簡單,但凡一個會基本電腦使用人上網Google拼拼湊湊就能組合出一個外觀不錯的”模型”。但要真正做出好看實用的”藝術品”,豐富的相關知識儲備還是必要的。就如同武俠小說裡內功深厚的人往往修練神功的時間不僅比別人短,還學得通透。學習網頁開發前需要先修煉的內功心法之一便是本文所整理的知識,瀏覽器事件循環。
0x01 行程(Process)和執行緒(Thread)
在開始講瀏覽器之前,要先了解CS中行程和執行緒。
行程(Process): 每個程式都需要一塊特別的記憶體空間來存放資訊,可以將這塊空間視作Process。一個運行中的程式至少都會有一個Process,當所有的Process停止運作時,程式就會終止。
執行緒(Thread): 如果把Process當作公司辦公室來看,執行緒就是員工。一個公司至少會有一個員工兼老闆,Process初始化時會建立一個Thread,我們稱之為Main thread(主執行緒),這也是很多程式語言的程式進入點叫做Main的原因。
當公司有很多工作需要同時進行時老闆一個人分身乏術,這時就需要招募員工了,每個員工各司其職(如會計、HR、業務及工程師等),這時老闆只要開會、做決策,躺著等數錢就好了(大誤)。舉個大家熟悉的例子,線上遊戲的程式同時需要畫面繪圖、監聽使用者的輸入及網路通訊等,這時就需要多個Thread來負責處理不同的任務。
0x02 瀏覽器的程式組成
了解Process和Thread的概念之後,我們來看看瀏覽器的組成。
現今瀏覽器的架構已經不是當年的IE那麼簡單,複雜程度堪比OS。(比如安全性,以前IE的AcitveX可以直接存取本地資源,而現在Broswer JS都是在沙盒環境執行,本地資源的存取被嚴格的管控)
程式複雜在運行的過程中就容易出錯,為了不讓一個錯誤讓整個程式Crash,瀏覽器會啟動多個Process來處理不同的任務。瀏覽器主要的Process為以下幾種:
- 瀏覽器(Browser) Process: 負責介面展示、使用者交互及子Process管理等。瀏覽器Process會啟動多個Thread來負責處理不同的任務。
- 網路(Network) Process: 負責載入網路資源,網路Process會啟動多個Thread來執行不同的任務。
- 渲染(Render) Process: 渲染Process初始化時會啟動一個渲染主執行緒(Main Thread),負責處理HTML、CSS及JS。瀏覽器預設每個標籤頁(Tab)會有各自的渲染Process,以免不同頁籤之間相互影響。
0x03 渲染主執行緒
渲染主執行緒是開發者最需要關注的地方,因為它與HTML/JS/CSS有者密切的關係。它主要負責工作有:
Parse HTML/CSS
Recaculate Style
Layout
Layer
Paint
Excute JS
Callbak/Timer Handling
…
在實際狀況下,渲染主執行緒可能會遇到以下問題:
- JS執行到一半,這時使用者觸發了交互事件,這時要先停下去處理交互事件嗎?
- JS執行到一半,此時計時器被觸發了,這時要先停下去處理計時器的回呼函數嗎?
- 使用者觸發交互事件的同時計時器被觸發了,這時該先處理哪個事件?
為了處理這類問題,瀏覽器引入了事件循環(Event loop)的機制。
0x04 同步(Sync)與非同步(Async)
在談論事件循環之前我們要先引入一個重要的概念,同步與非同步(也稱為異步)。
同步(Sync): 這是大家最熟悉的方式,程式從頭到尾一行行的執行。這種方式的優點是程式架構簡單易維護,缺點就是遇到耗時的操作(如等待I/O或網路請求),需等待該操作完成才能繼續其他的操作,程式的效率因而下降。
非同步(Async): 這種模式下,程式遇到耗時操作可以先去執行其他任務,當耗時任務完成時會透過某種機制告知程式繼續執行接下來的操作。採用非同步模式通常可以提高程式的效率與同時響應數,但相對的缺點就是程式架構變得複雜難以維護(如需考慮資源競爭與死鎖等問題)。
換句話來說,同步和非同步模式就像餐廳的服務模式,握壽司店的師傅會將一個客人點的壽司捏完擺到客人桌上後才去服務下一位客人。西餐廳會由服務生點單後交由廚房料理,廚房料理後再交由服務生上菜。
非同步有很多實現的方式,常見的有以下幾種:
- 非同步輸入/輸出(Asynchronous I/O)
- 非同步程序調用(Asynchronous Procedure Call, APC)
- 非同步方法調度(Asynchronous Method Dispatch, AMD)
- Promise/Future
- 事件驅動編程
最後一種名稱是不是跟我們的事件循環有點類似,是的你沒想錯,事件驅動是一種基於事件驅動編程實現的機制。
0x05 事件循環
前面鋪墊了這麼久,終於輪到我們今天的主角 - 事件循環登場了。事件循環的英文為Event loop(在Chrome C++原始碼內稱為Message loop),瀏覽器採用這種非同步機制來讓JS執行時不會造成阻塞(在適當使用下)。
渲染主執行緒在初始化後會先進入一個無限迴圈,這個迴圈每次執行時會檢查瀏覽器的訊息佇列(Message queue, MQ)中是否有任務存在,若有任務存在,就將任務自MQ取出消費(執行任務),任務執行完後進入下一次循環。若無任務存在於MQ中,執行緒會進入休眠狀態,等待被喚醒。
其他執行緒可以隨時將任務加入MQ末端,在加入任務的時候若渲染主執行緒處於休眠狀態,會將它壞醒以繼續執行任務。
舉個例子,在JS遇到計時任務,渲染主執行緒時會另外啟動一個Thread用來計時,這時任務對於主執行緒來說就已經結束,他會直接去執行接下來的任務。計時用的Thread在時間到時會將Callback程式包裝成任務送至MQ末端來讓主執行緒接著執行Callback function的程式。
0x06 為什麼JS是單執行緒,JS的非同步又是什麼
JavaScript之所以是單執行緒的其實是因為它的運行環境導致(瀏覽器的渲染主執行緒),因為執行緒只有一個,HTML/CSS解析、畫面渲染及JS執行都是在此執行緒中完成。若是採用同步的方式執行遇到耗時就會造成阻塞,在MQ的任務會無法及時進行,使用者更是有可能看到畫面卡住的狀況。遇到這類情況(如網路請求、事件監聽及計時器等),主執行緒會採用非同步的方式來執行JS(如先前介紹的計時器Thread的執行方式)。
0x07 執行的優先順序
之前提到渲染主執行緒會將MQ的任務取出消費,那這些任務有沒有優先順序呢?答案是沒有,但瀏覽器用另一種方式來實現優先順序的機制。根據W3C的定義:
- 每個任務都有任務類型,同一類型的任務必須存放在同一Queue
- 每次事件循環時,瀏覽器可依據實際狀況從不同Queue中取出任務執行
- 每個Broswer必須有一個微佇列,微佇列中的任務優先及高於所有其他類型的任務
在Chrome的原始碼中,三種主要Queue的實現:
- 微佇列: 這裡的任務清空了才會去執行其他Queue裡的任務,優先級最高。
- 交互佇列: 在Chrome除了微任務外,使用者的交互是最重要的,優先級為高。
- 延時佇列: 用於執行計時任務的Callback function,優先級為中。
0x08 JS的不當使用會造成阻塞
TBD
0x09 JS裡的計時器誤差
TBD
