事件循環(huán)模型
事件循環(huán)老生常談了,社區(qū)的相關文章也非常多了药磺,但這次為了徹底搞懂告组,我深度查看了規(guī)范中的規(guī)則以及 Chromium中的源碼實現(xiàn)。本文章作為筆記記錄癌佩。
本文主要參考了
WHATWG
規(guī)范中的定義惹谐。建議直接從規(guī)范中學習。
各家瀏覽器對事件循環(huán)的具體實現(xiàn)會有細微差別驼卖,本文所有代碼輸出均使用 chrome氨肌。
面試碰到很多問題,比如下面這些酌畜,面試官實際上很可能想問的是 事件循環(huán)怎囚,搞明白了之后就不用害怕被面試官牽著走了,甚至可以主動引導面試官問事件循環(huán)桥胞。
- 什么是進程恳守?什么是線程?
- 為什么 js 是異步的贩虾?
- 說說 Promise解決了什么問題催烘?
- 什么是微任務和宏任務?
- js 引擎執(zhí)行代碼的順序是什么缎罢?
- dom 點擊事件中有大量計算后更新 dom伊群,會有卡死現(xiàn)象如何優(yōu)化?
- 為什么 dom 點擊事件中有大量的計算策精,即使使用異步舰始,還是會影響用戶交互操作?
- JS 能實現(xiàn)精準計時器嗎咽袜?
- 如何實現(xiàn)一個盡可能精準的計時器丸卷?
- 為什么說 js 是事件驅動的?
- …
瀏覽器的進程模型
進程 (Process)
定義
進程是一個正在執(zhí)行的程序的實例询刹。它包含程序代碼谜嫉、數(shù)據(jù)萎坷、資源(如文件、內存)以及執(zhí)行中的程序計數(shù)器沐兰、寄存器和堆棧哆档。
特點
- 獨立性:進程是獨立的執(zhí)行單元,擁有自己的內存空間和資源僧鲁。一個進程不能直接訪問另一個進程的內存。
- 資源開銷:創(chuàng)建和銷毀進程的開銷較大象泵,因為操作系統(tǒng)需要分配和管理獨立的資源寞秃。
- 安全性:由于進程之間相互獨立,不同進程之間的錯誤不會直接影響彼此偶惠,提高了系統(tǒng)的穩(wěn)定性春寿。
示例
操作系統(tǒng)中的每個運行的應用程序,如文本編輯器忽孽、瀏覽器或計算器绑改,都是一個進程。
線程 (Thread)
定義
線程是進程中的一個執(zhí)行路徑兄一,也被稱為輕量級進程(Lightweight Process, LWP)厘线。一個進程可以包含多個線程,它們共享進程的內存和資源出革。
特點
- 共享資源:同一個進程內的線程共享內存和資源造壮,因此線程間通信和數(shù)據(jù)共享更加容易和高效。
- 開銷較小:創(chuàng)建和銷毀線程的開銷比進程小骂束,因為線程之間共享進程的資源耳璧。
- 并行執(zhí)行:多線程允許一個進程中的多個任務并行執(zhí)行,從而提高程序的執(zhí)行效率展箱。
示例
在一個文本編輯器中旨枯,可能會有一個線程負責響應用戶輸入,另一個線程負責自動保存文檔混驰,還有一個線程負責拼寫檢查攀隔。
進程與線程的對比
特性 | 進程 | 線程 |
---|---|---|
內存空間 | 獨立 | 共享進程內存 |
創(chuàng)建開銷 | 大 | 小 |
通信方式 | 通過進程間通信(IPC) | 通過共享內存 |
崩潰影響 | 獨立進程崩潰不影響其他進程 | 線程崩潰可能導致整個進程崩潰 |
執(zhí)行效率 | 較低(獨立內存) | 較高(共享內存) |
進程與線程的使用場景
- 進程:適用于需要高隔離性和穩(wěn)定性的任務。例如栖榨,操作系統(tǒng)中的不同應用程序通常運行在獨立的進程中竞慢。
- 線程:適用于需要高效并行執(zhí)行且可以共享資源的任務。例如治泥,服務器應用程序中的每個請求可以由一個獨立的線程處理筹煮。
總結
- 進程是操作系統(tǒng)中獨立運行的程序實例,擁有獨立的內存空間和資源居夹,適用于需要高隔離性和穩(wěn)定性的任務败潦。創(chuàng)建和銷毀進程開銷較大本冲,但穩(wěn)定性高。
- 線程是進程中的執(zhí)行路徑劫扒,共享進程的內存和資源檬洞,適用于需要高效并行執(zhí)行的任務。線程創(chuàng)建和銷毀開銷較小沟饥,但一個線程崩潰可能影響整個進程添怔。
總結來說,進程提供隔離性和穩(wěn)定性贤旷,而線程提供高效的并行執(zhí)行能力广料。
瀏覽器有哪些進程和線程
瀏覽器是一個多進程多線程的應用程序,而 js 則是單線程的幼驶。
-
主進程
負責瀏覽器的用戶界面(比如瀏覽器上面的地址欄艾杏、前進后退、書簽管理等等)盅藻、管理各個子進程(管理和創(chuàng)建渲染進程)购桑、處理用戶輸入以及與操作系統(tǒng)的交互(如文件訪問)
-
渲染進程
每個標簽頁、iframe 都是一個獨立的渲染進程氏淑。渲染進程需要處理非常多的任務勃蜘,譬如:
- 解析 HTML
- 構建 DOM 樹
- 解析 CSS
- 計算樣式
- 計算布局
- 頁面繪制(重排重繪)
- 每秒渲染 60 次頁面
- 執(zhí)行 JS 代碼,處理宏任務微任務
- …
-
網絡進程
專門負責網絡請求和資源下載假残,獨立于主進程和渲染進程
-
GPU進程
負責處理圖形相關任務元旬,如3D繪圖和硬件加速,以提高渲染性能
-
插件進程
瀏覽器插件運行在獨立的插件進程中守问,負責處理插件相關的任務匀归。
…
在瀏覽器的每個渲染進程中,通常包括以下線程:
-
主線程:
- 執(zhí)行 JavaScript
- 處理事件循環(huán)
- 執(zhí)行布局和繪制
-
渲染線程:
- 負責頁面的繪制
-
合成線程:
- 處理 CSS 動畫和合成層
-
光柵化線程:
- 將合成層轉換為位圖
-
網絡線程:
- 處理網絡請求
-
Worker 線程:
- 用于 Web Worker 和 Service Worker 的執(zhí)行
異步編程
眾所周知耗帕,JavaScript在瀏覽器的主線程中是單線程執(zhí)行的穆端。而異步編程允許代碼在不阻塞主線程的情況下執(zhí)行耗時操作。
同步編程帶來的問題
在瀏覽器環(huán)境中仿便,每個標簽頁都有其獨立的渲染進程体啰,其中包含一個主線程負責處理多項任務,如解析 HTML嗽仪、構建 DOM 樹荒勇、解析 CSS、計算樣式和布局闻坚、渲染頁面以及執(zhí)行 JavaScript 代碼等沽翔。
如果所有這些任務都同步執(zhí)行,可能會導致以下問題:
- 主線程效率低下:例如,在等待 AJAX 請求返回結果時仅偎,主線程處于空閑狀態(tài)跨蟹。
- 用戶體驗差:長時間的計算或等待可能導致頁面無響應,影響交互橘沥。
為解決這些問題窗轩,瀏覽器引入了異步編程模型處理各種類型的任務,確保了主線程不會被阻塞座咆,提高了程序的整體效率和響應性痢艺。事件循環(huán)就是異步的實現(xiàn)方式。
JavaScript 中的任務類型
同步代碼介陶、微任務堤舒、宏任務的執(zhí)行順序是什么?
同步代碼
在初始階段斤蔓,script 標簽中的代碼被包裝稱為一個宏任務放到任務隊列中植酥,此時是沒有其他微任務的镀岛。在全局代碼中按照出現(xiàn)的順序立即執(zhí)行的都是同步代碼弦牡。
異步代碼則分為微任務和宏任務。
微任務
在 JavaScript 中漂羊,微任務(Microtasks)是為了在當前事件循環(huán)結束之前執(zhí)行的小任務驾锰。
微任務在 JavaScript 執(zhí)行堆棧為空時運行;微任務的執(zhí)行優(yōu)先級高于宏任務(Macrotasks)走越。另外椭豫,W3C 規(guī)范中規(guī)定每個渲染進程(標簽頁或 Web Worker )中必須且只能有一個微任務隊列!
不理解也沒關系旨指,只需要記住下面的微任務即可
-
Promise 回調函數(shù)
通過
.then()
赏酥、.catch()
、.finally()
注冊的回調函數(shù)是微任務谆构。但Promise本身不是微任務裸扶。例如:下面代碼中
console.log('Promise');
屬于全局同步代碼,并不會進入微任務隊列搬素!console.log('Script start'); setTimeout(() => { console.log('setTimeout'); }, 0); new Promise((resolve, reject) => { console.log('Promise'); resolve(); }).then(() => { console.log('then'); }); // 監(jiān)聽 DOM 變化的回調 const observer = new MutationObserver(() => { // 該回調函數(shù)會進入為任務隊列 console.log('MutationObserver'); }); observer.observe(document.querySelector('.box'), { attributes: true }); document.querySelector('.box').setAttribute('data', Math.random()); Promise.resolve() .then(() => { console.log('Promise 1'); }) .then(() => { console.log('Promise 2'); }); console.log('Script end'); // 執(zhí)行結果: // Script start // Promise // Script end // then // MutationObserver // Promise 1 // Promise 2 // setTimeout
-
MutationObserver 的回調函數(shù)
當監(jiān)聽 DOM 變化呵晨,其觸發(fā)的回調函數(shù)是微任務。參考上面的代碼
console.log('MutationObserver');
是先進入微任務隊列后按隊列順序執(zhí)行的熬尺。 -
queueMicrotask 的回調函數(shù)
顯式將任務添加到微任務隊列摸屠。可以用來改變某些代碼的執(zhí)行順序粱哼,但如果增加過多
queueMicrotask
季二,可能導致其他任務無法執(zhí)行或延遲, 比如頁面渲染任務揭措。console.log('Script start'); setTimeout(() => { console.log('setTimeout'); }, 0); new Promise((resolve, reject) => { console.log('Promise'); resolve(); }).then(() => { console.log('then'); }); // 如果執(zhí)行下面的無限遞歸addMicrotask // 那么頁面上.box textContent永遠也不會變化戒傻,渲染 dom 是一個宏任務 // 微任務沒有執(zhí)行完税手,控制權不會交換給事件循環(huán),因此無法執(zhí)行宏任務 // function addMicrotask() { // queueMicrotask(() => { // console.log('Microtask executed'); // addMicrotask(); // 繼續(xù)添加微任務 // }); // } // addMicrotask() queueMicrotask(() => { console.log('Microtask 1'); }); const observer = new MutationObserver(() => { console.log('MutationObserver'); }); observer.observe(document.querySelector('.box'), { attributes: true, }); document.querySelector('.box').textContent = +Date.now(); console.log('Script end'); // 執(zhí)行結果 // Script start // Promise // Script end // then // Microtask 1 // setTimeout
await
使用 await
時需纳,函數(shù)會暫停執(zhí)行芦倒,直到 await
的 Promise 解決(fulfilled 或 rejected),這段暫停時間允許其他任務執(zhí)行,不會阻塞主線程不翩。
當 Promise 解決時兵扬,await
后面的代碼會作為微任務加入微任務隊列。
宏任務
在最新的 W3C(WHATWG) 規(guī)范中口蝠,其實并沒有宏任務的定義器钟,而是直接稱之為“任務”,為了容易區(qū)分妙蔗,我們暫時還稱為宏任務傲霸,但請記住,規(guī)范的說法是“任務”眉反。
宏任務(Macrotasks)同樣用于處理異步操作昙啄,他和微任務的最大區(qū)別在于執(zhí)行時機,宏任務在微任務之后執(zhí)行寸五。js 中主要有以下宏任務:
- setTimeout/setInterval 定時器回調函數(shù)
- Node.js 中的 setImmediate
- I/O 操作: 處理文件讀寫梳凛、網絡請求等輸入輸出操作。
- UI 渲染: 頁面繪制梳杏、DOM更新等等
- MessageChannel: 可以在不同的瀏覽器上下文之間持續(xù)雙向通信
- postMessage: 在不同窗口韧拒、iframe、或 worker 之間傳遞單個消息十性,單向通信
- 事件回調: 綁定到 DOM 事件叛溢,如點擊、輸入等
其他特殊任務
事實上劲适,還有一些特殊的任務楷掉,他們既不是微任務也不是宏任務,有自己獨立的運行機制减响,不適用于常規(guī)的事件循環(huán)機制靖诗,比如:
- requestIdleCallback: 在瀏覽器空閑時執(zhí)行代碼的 API,優(yōu)先級較低
- requestAnimationFrame: 瀏覽器在下一次重繪之前支示,調用用戶提供的回調函數(shù)刊橘。它的優(yōu)先級高于宏任務,但低于微任務
任務隊列
任務隊列在規(guī)范中有明確的說明:
- 事件循環(huán)有一個或多個任務隊列颂鸿。任務隊列是一組任務促绵。
- 任務隊列是集合,而不是隊列,因為事件循環(huán)處理模型從所選隊列中獲取第一個可運行的任務败晴,而不是使第一個任務出隊浓冒。
- 微任務隊列不是任務隊列(這里指的是“宏”任務隊列)。
微任務隊列
微任務隊列是當前 JavaScript 主線程中所有微任務的集合尖坤。規(guī)范要求每個事件循環(huán)都有一個微任務隊列稳懒,最初是空的。
在所有的同步任務執(zhí)行完成后慢味,控制權移交事件循環(huán)场梆,并獲取微任務隊列中第一個可運行的任務創(chuàng)建執(zhí)行上下文后在執(zhí)行堆棧中運行。
任務隊列
和上面所說的宏任務一樣纯路,規(guī)范中的準確定義是
任務隊列
或油。
任務隊列是當前 JavaScript 主線程中所有微任務的集合。 但和為任務隊列不同的是每個事件循環(huán)中可以有多個任務隊列驰唬。
當頁面加載或腳本執(zhí)行時顶岸,最初的同步代碼被視為一個宏任務。而此時執(zhí)行棧是空的叫编,微任務隊列也是空的辖佣,所以這個“宏任務”會優(yōu)先執(zhí)行。
其他的情況則是按照 執(zhí)行同步代碼 → 執(zhí)行微任務 → 執(zhí)行宏任務的順序宵溅。
使用定時器無法實現(xiàn)精準的定時效果凌简,給他們傳入延時參數(shù)只是最快執(zhí)行的時間上炎,而實際上即使計時到了恃逻,也必須等待所有的同步代碼和微任務執(zhí)行完成。
另外藕施,定時器嵌套達到5層后會延時參數(shù)最小值會強制從0變?yōu)?寇损,這也增大了誤差。
規(guī)范中定義了多個任務的類型裳食,他們有自己關聯(lián)的任務隊列矛市,比如,可能有以下隊列:
- DOM 操作隊列: 處理 DOM 相關的任務
- 網絡事件隊列: 處理網絡請求的響應
-
計時器隊列: 處理
setTimeout
和setInterval
- 用戶交互隊列: 處理用戶輸入事件诲祸,如點擊和鍵盤輸入浊吏。
- 渲染隊列: 處理與頁面渲染相關的任務。
多個任務隊列如何保證執(zhí)行順序救氯?通常來說是按照進入隊列的時間找田,不過規(guī)范中并沒有嚴格定義,允許各家瀏覽器自行實現(xiàn)內部細節(jié)着憨,但用戶交互相關任務(如鼠標點擊墩衙、鍵盤輸入等)的優(yōu)先級會較高一些,以確保用戶操作的響應速度。
規(guī)范中的原文如下:
For example, a user agent could have one task queue for mouse and key events (to which the user interaction task source is associated), and another to which all other task sources are associated. Then, using the freedom granted in the initial step of the event loop processing model, it could give keyboard and mouse events preference over other tasks three-quarters of the time, keeping the interface responsive but not starving other task queues. Note that in this setup, the processing model still enforces that the user agent would never process events from any one task source out of order.
任務派發(fā)流程
在瀏覽器環(huán)境中漆改,雖然 JavaScript 是單線程執(zhí)行的心铃,但每個渲染進程(如每個 Tab 或 Worker)由多個線程組成。這些線程包括:
- 主線程(UI 線程):執(zhí)行 JavaScript挫剑、布局去扣、繪制等。
- 合成線程: 處理頁面合成和光柵化樊破。
- 工作線程(Web Workers):包括Web Workers和其他后臺任務處理線程厅篓。
- IO線程: 處理IPC通信和網絡請求。
-
定時器線程:處理定時任務(如
setTimeout
捶码、setInterval
)羽氮。
當JavaScript代碼在主線程的執(zhí)行棧中運行時,遇到異步API(如定時器惫恼、網絡請求档押、事件監(jiān)聽器等)會觸發(fā)任務分發(fā):
- 定時器任務: 由定時器線程管理,到期后將回調函數(shù)封裝為任務祈纯,放入任務隊列令宿。
- 網絡請求: 由IO線程處理,完成后將回調封裝為任務腕窥,加入網絡任務隊列粒没。
- DOM事件: 由主線程監(jiān)聽,事件觸發(fā)時將監(jiān)聽器封裝為任務簇爆,加入事件任務隊列癞松。
- Promise微任務: 在當前執(zhí)行棧清空后,主線程立即處理這些微任務入蛆。
接下來等待事件循環(huán)處理任務隊列即可响蓉。
- 瀏覽器內部會有一套非常復雜的邏輯用于管理不同優(yōu)先級的任務隊列,例如chromium 中的 sequence_manger
事件循環(huán)
在 w3c 標準中稱為
Event loops
哨毁,而在谷歌的 chromium 中稱為Message Loop
枫甲,源碼中具體的實現(xiàn)方法是MessagePumpDefault::Run
,參考以下代碼:
void MessagePumpDefault::Run(Delegate* delegate) {
// 通過AutoReset類自動管理keep_running_的值扼褪。構造函數(shù)將keep_running_設置為true想幻,
// 析構函數(shù)將其恢復為原來的值。
AutoReset<bool> auto_reset_keep_running(&keep_running_, true);
// 無限循環(huán)话浇,直到keep_running_被設置為false或遇到break語句脏毯。
for (;;) {
#if BUILDFLAG(IS_APPLE)
// 在Apple平臺上,創(chuàng)建一個自動釋放池(autorelease pool)凳枝,
// 用于管理Objective-C對象的內存抄沮。
apple::ScopedNSAutoreleasePool autorelease_pool;
#endif
// 調用delegate的DoWork方法獲取下一步工作的信息跋核。
Delegate::NextWorkInfo next_work_info = delegate->DoWork();
// 檢查是否有更多的緊急工作需要立即處理。
bool has_more_immediate_work = next_work_info.is_immediate();
// 如果keep_running_被設置為false叛买,退出循環(huán)砂代。
if (!keep_running_)
break;
// 如果有更多的緊急工作,繼續(xù)循環(huán)處理率挣,而不進行等待刻伊。
if (has_more_immediate_work)
continue;
// 調用delegate的DoIdleWork方法,處理空閑時的工作椒功。
delegate->DoIdleWork();
// 再次檢查keep_running_捶箱,如果為false,則退出循環(huán)动漾。
if (!keep_running_)
break;
// 根據(jù)next_work_info中的信息決定是否進行等待丁屎。
if (next_work_info.delayed_run_time.is_max()) {
// 如果next_work_info.delayed_run_time為最大值,
// 則進行無限等待旱眯,直到event_被觸發(fā)晨川。
event_.Wait();
} else {
// 否則,等待指定的時間(remaining_delay)后再繼續(xù)删豺。
event_.TimedWait(next_work_info.remaining_delay());
}
// Since event_ is auto-reset, we don't need to do anything special here
// other than service each delegate method.
// event_是自動重置的共虑,因此我們不需要在這里做特殊處理。
// 只需要繼續(xù)服務每個delegate的方法呀页。
}
}
了解上面的內容后妈拌,事件循環(huán)就非常容易理解了。整體流程如下:
- 主線程運行全局/局部同步代碼蓬蝶,同時處理微任務隊列和任務隊列
- 同步代碼執(zhí)行完成通知事件循環(huán)啟動并查詢微任務隊列中第一個可運行的任務交由執(zhí)行棧運行代碼尘分,直至微任務隊列為空
- 查詢其他任務隊列,取出第一個可運行任務交由執(zhí)行棧運行代碼
- 宏任務中可能存在同步代碼或微任務疾党,重復1-4音诫,直至任務隊列全部清空
Javascript 是事件驅動的惨奕。
代碼輸出順序
下面這道地獄級別輸出題雪位,能答對70%我相信就能應對大部分面試官了??(可以去掉requestAnimationFrame和requestIdleCallback,這兩個干擾比較大)
這個案例中從輸出結果來看channel.port2.postMessage的優(yōu)先級似乎比 settimeout 要低一些梨撞,并沒有嚴格按照代碼出現(xiàn)順序執(zhí)行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Macro and Microtasks</title>
<style></style>
</head>
<body>
<textarea name="" id=""></textarea>
<div class="box"></div>
<script>
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
setTimeout(() => {
console.log("3");
}, 1000);
const channel = new MessageChannel();
channel.port1.onmessage = (val) => {
console.log(val);
};
channel.port2.postMessage("4");
setInterval(() => {
console.log("55");
}, 1000);
let a = 1;
while (a < 100000) {
a++;
if (a == 1000 || a == 100000) console.log(a);
}
new Promise(function (resolve, reject) {
console.log("5");
resolve(3);
}).then(function (val) {
console.log("6");
});
console.log(7);
Promise.resolve()
.then(() => {
console.log("8");
})
.then(() => {
console.log("9");
});
const observer = new MutationObserver(() => {
console.log("MutationObserver 10");
});
observer.observe(document.querySelector(".box"), {
attributes: true,
});
document.querySelector(".box").setAttribute("data", Math.random());
setTimeout(() => {
console.log("11");
Promise.resolve().then(() => {
console.log("12");
});
requestAnimationFrame(() => {
console.log("RAF 13");
});
}, 0);
console.log("14");
requestIdleCallback(() => {
console.log("requestIdleCallback 15");
});
queueMicrotask(() => {
console.log("16");
new Promise((resolve, reject) => {
console.log("17");
resolve();
}).then(() => {
console.log("18");
});
Promise.resolve()
.then(() => {
console.log("19");
})
.then(() => {
console.log("20");
});
setTimeout(() => {
console.log("21");
}, 0);
requestAnimationFrame(() => {
console.log("RAF 22");
});
});
Promise.resolve()
.then(() => {
console.log("23");
})
channel.port2.postMessage("24");
requestAnimationFrame(() => {
console.log("RAF 25");
});
console.log("26");
</script>
</body>
</html>
參考資料
WHATWG
雖然W3C曾是Web標準的主要制定者雹洗,但在HTML方面,WHATWG的“Living Standard”模式更適合快速變化的Web環(huán)境卧波。W3C的標準更新較慢时肿,可能無法及時反映最新的技術和瀏覽器實現(xiàn)。最重要的是港粱,各大瀏覽器廠商通常使用WHATWG的規(guī)范來實現(xiàn)HTML和DOM標準螃成。
資料列表
- 事件循環(huán) - HTML Standard
- 任務隊列 - HTML Standard
- 任務源類型 - HTML Standard
- 事件循環(huán)執(zhí)行步驟 - HTML Standard
- 深入:微任務與 Javascript 運行時環(huán)境 - Web API | MDN
- 并發(fā)模型與事件循環(huán) - JavaScript | MDN
- 在 JavaScript 中通過 queueMicrotask() 使用微任務 - Web API | MDN
- base::CurrentTaskRunner 提案
- Chromium Docs - Threading and Tasks in Chrome
- chromium/base/task/sequence_manager/README.md · chromium/chromium
- Chrome 瀏覽器中的事件循環(huán)工作原理 | 作者:Roman Melnik
- 瀏覽器線程 | Cycle263 Blog