JS的運(yùn)行機(jī)制之事件循環(huán)機(jī)制示例詳解
前言
在現(xiàn)代Web開(kāi)發(fā)中,JavaScript是構(gòu)建動(dòng)態(tài)交互體驗(yàn)的核心。但它的運(yùn)行機(jī)制卻常被誤解——一個(gè)看似“單線(xiàn)程”的語(yǔ)言如何高效處理復(fù)雜的異步操作?JavaScript 是一門(mén)以事件驅(qū)動(dòng)、單線(xiàn)程執(zhí)行為核心特征的編程語(yǔ)言。盡管它在執(zhí)行代碼時(shí)只有一個(gè)主線(xiàn)程,但卻能高效地處理各種異步任務(wù),如定時(shí)器、網(wǎng)絡(luò)請(qǐng)求、用戶(hù)交互等。這背后,正是強(qiáng)大的事件循環(huán)機(jī)制在默默運(yùn)轉(zhuǎn)。本文將從瀏覽器的進(jìn)程模型開(kāi)始,逐步揭開(kāi) JavaScript 事件循環(huán)的神秘面紗。
一、進(jìn)程和線(xiàn)程
1.1進(jìn)程和線(xiàn)程的基本概念
要想了解JavaScript的運(yùn)行機(jī)制,首先得了解進(jìn)程和線(xiàn)程是什么
進(jìn)程(Process)是操作系統(tǒng)分配資源的基本單位
線(xiàn)程(Thread) 是程序執(zhí)行的最小單位,一個(gè)進(jìn)程中可以包含多個(gè)線(xiàn)程,共享內(nèi)存資源。
一個(gè)應(yīng)用至少一個(gè)進(jìn)程,每個(gè)進(jìn)程之間相互獨(dú)立,進(jìn)程之間共享內(nèi)存空間。
一個(gè)進(jìn)程至少一個(gè)線(xiàn)程,進(jìn)程開(kāi)啟后會(huì)自動(dòng)創(chuàng)建一個(gè)線(xiàn)程來(lái)運(yùn)行代碼,稱(chēng)為主線(xiàn)程。要是程序需要同時(shí)執(zhí)行多塊代碼,就需要啟動(dòng)動(dòng)更多線(xiàn)程來(lái)執(zhí)行代碼,所以一個(gè)進(jìn)程可以包含多個(gè)進(jìn)程。
而JavaScript本身就是 運(yùn)行在瀏覽器中,而瀏覽器本身也是一個(gè)多進(jìn)程、多線(xiàn)程的復(fù)雜應(yīng)用
1.2 瀏覽器的多進(jìn)程架構(gòu)
現(xiàn)代瀏覽器(如 Chrome)采用多進(jìn)程架構(gòu),其主要進(jìn)程包括:
瀏覽器主進(jìn)程:負(fù)責(zé)地址欄、書(shū)簽欄、前進(jìn)后退、標(biāo)簽管理等 UI 操作。
網(wǎng)絡(luò)進(jìn)程:處理網(wǎng)絡(luò)資源的下載(如 HTML、CSS、JS、圖片等)。
渲染進(jìn)程:每打開(kāi)一個(gè)標(biāo)簽頁(yè),通常會(huì)分配一個(gè)獨(dú)立的渲染進(jìn)程,負(fù)責(zé)頁(yè)面渲染與 JavaScript 執(zhí)行。
GPU進(jìn)程:處理與圖形渲染相關(guān)的任務(wù),包括3D CSS效果、頁(yè)面UI的GPU加速繪制,以及視頻解碼等
插件進(jìn)程(Plugin Process):獨(dú)立運(yùn)行瀏覽器插件(如Flash、廣告攔截工具),防止插件崩潰影響瀏覽器或其他頁(yè)面
輔助進(jìn)程(根據(jù)場(chǎng)景啟動(dòng)):跨頁(yè)面共享的JavaScript線(xiàn)程,獨(dú)立于渲染進(jìn)程管理
瀏覽器插件種類(lèi)繁多,但我們需要重點(diǎn)掌握J(rèn)avaScript的運(yùn)行機(jī)制。這就要從關(guān)鍵的渲染進(jìn)程入手,它與前端頁(yè)面渲染密切相關(guān)。
1.3 渲染進(jìn)程中的主線(xiàn)程
渲染進(jìn)程內(nèi)部,核心是渲染主線(xiàn)程,它負(fù)責(zé):
解析 HTML,構(gòu)建 DOM 樹(shù)
解析 CSS,構(gòu)建 CSSOM
執(zhí)行 JavaScript
計(jì)算樣式、布局、繪制
JavaScript 的運(yùn)行環(huán)境就在這個(gè)主線(xiàn)程中。因此,JavaScript 是單線(xiàn)程執(zhí)行。
二、異步機(jī)制
JavaScript 只運(yùn)行在瀏覽器的渲染主線(xiàn)程中,渲染主線(xiàn)程就只有一個(gè),而這個(gè)線(xiàn)程既要負(fù)責(zé)腳本執(zhí)行,又要承擔(dān)頁(yè)面渲染、事件響應(yīng)等工作。試想一下,如果我們讓 JS 主線(xiàn)程一直執(zhí)行某個(gè)任務(wù)(比如復(fù)雜的循環(huán)),那其他任務(wù)(如用戶(hù)點(diǎn)擊)就無(wú)法響應(yīng),頁(yè)面也無(wú)法渲染更新,用戶(hù)體驗(yàn)會(huì)極差。
2.1 同步的代價(jià)
我們先來(lái)分析一段代碼
while (true) { // 模擬高消耗運(yùn)算 }
這段代碼是一個(gè)死循環(huán),它將讓頁(yè)面徹底卡死,因?yàn)橹骶€(xiàn)程被死循環(huán)阻塞,無(wú)法響應(yīng)任何操作。
2.2 異步機(jī)制的引入
為了防止主線(xiàn)程長(zhǎng)時(shí)間阻塞,JavaScript 借助瀏覽器的其他線(xiàn)程(如計(jì)時(shí)器線(xiàn)程、網(wǎng)絡(luò)線(xiàn)程等)來(lái)異步處理任務(wù):
1.主線(xiàn)程將異步任務(wù)交由其他線(xiàn)程執(zhí)行(如 setTimeout 的計(jì)時(shí)由 Web APIs 中的 Timer 線(xiàn)程負(fù)責(zé))。
2.等任務(wù)完成后,將“回調(diào)函數(shù)”包裝成任務(wù)加入任務(wù)隊(duì)列。
3.主線(xiàn)程空閑后,從任務(wù)隊(duì)列中取出這些任務(wù)執(zhí)行。
這樣就實(shí)現(xiàn)了非阻塞的異步模型。
2.3 異步機(jī)制的解讀
JavaScript 是一門(mén)單線(xiàn)程語(yǔ)言,因?yàn)樗\(yùn)行在瀏覽器的渲染主線(xiàn)程上,而該線(xiàn)程是唯一的。
渲染主線(xiàn)程需要處理多項(xiàng)任務(wù),包括頁(yè)面渲染和執(zhí)行 JavaScript 代碼等。如果采用同步執(zhí)行方式,可能會(huì)導(dǎo)致主線(xiàn)程阻塞,進(jìn)而使消息隊(duì)列中的其他任務(wù)無(wú)法及時(shí)執(zhí)行。這不僅會(huì)造成主線(xiàn)程資源的浪費(fèi),還會(huì)導(dǎo)致頁(yè)面更新延遲,給用戶(hù)帶來(lái)卡頓的體驗(yàn)。
為此,瀏覽器采用異步機(jī)制來(lái)解決這個(gè)問(wèn)題。當(dāng)遇到計(jì)時(shí)器、網(wǎng)絡(luò)請(qǐng)求、事件監(jiān)聽(tīng)等任務(wù)時(shí),主線(xiàn)程會(huì)將這些任務(wù)交由其他線(xiàn)程處理,自身則繼續(xù)執(zhí)行后續(xù)代碼。待其他線(xiàn)程完成任務(wù)后,會(huì)將回調(diào)函數(shù)封裝成新任務(wù),添加到消息隊(duì)列末尾等待主線(xiàn)程調(diào)度執(zhí)行。
這種異步機(jī)制有效避免了瀏覽器阻塞,確保了單線(xiàn)程運(yùn)行的流暢性。
三、事件循環(huán)機(jī)制
3.1 事件循環(huán)(event loop)
事件循環(huán)是瀏覽器或 Node.js 中控制異步執(zhí)行的核心機(jī)制。它的本質(zhì)就是一個(gè)不斷循環(huán)的系統(tǒng):
while (true) { // 1. 執(zhí)行一個(gè)宏任務(wù)(task) // 2. 執(zhí)行完立即清空所有微任務(wù)(microtasks) // 3. 如果需要更新界面,則進(jìn)行渲染 }
每一次完整的循環(huán)稱(chēng)為一個(gè)“tick”。在每個(gè) tick 中,主線(xiàn)程都會(huì)從任務(wù)隊(duì)列中取出一個(gè)宏任務(wù)執(zhí)行,并在其執(zhí)行完畢后立即清空微任務(wù)隊(duì)列。
3.2 任務(wù)隊(duì)列
現(xiàn)代瀏覽器不再僅使用“宏任務(wù)隊(duì)列 + 微任務(wù)隊(duì)列”的模型,而是根據(jù)任務(wù)來(lái)源將宏任務(wù)進(jìn)一步拆分成多個(gè)隊(duì)列,例如:
延時(shí)隊(duì)列:setTimeout, setInterval
用戶(hù)交互隊(duì)列:如 click、input 等事件
UI 渲染隊(duì)列:requestAnimationFrame
網(wǎng)絡(luò)事件隊(duì)列:fetch、xhr
這些消息隊(duì)列中的任務(wù)采用先進(jìn)先出機(jī)制,沒(méi)有獨(dú)立優(yōu)先級(jí),但消息隊(duì)列本身具有優(yōu)先級(jí)劃分。
每個(gè)任務(wù)都有特定類(lèi)型,同類(lèi)型任務(wù)必須歸入同一隊(duì)列,不同類(lèi)型任務(wù)可分配到不同隊(duì)列。在單次事件循環(huán)中,瀏覽器可根據(jù)實(shí)際情況從不同隊(duì)列中選取任務(wù)執(zhí)行。
瀏覽器必須維護(hù)專(zhuān)門(mén)的微隊(duì)列,該隊(duì)列中的任務(wù)享有最高執(zhí)行優(yōu)先級(jí)。
當(dāng)前主流隊(duì)列按優(yōu)先級(jí)排序?yàn)椋貉訒r(shí)隊(duì)列(中優(yōu)先級(jí))、交互隊(duì)列(高優(yōu)先級(jí))、微隊(duì)列(最高優(yōu)先級(jí))。
3.3 瀏覽器中一輪事件循環(huán)的過(guò)程
根據(jù)上述對(duì)事件循環(huán)和異步任務(wù)的解釋?zhuān)梢缘贸鰹g覽器中的一輪事件的循環(huán)過(guò)程
取出一個(gè)宏任務(wù)執(zhí)行
執(zhí)行過(guò)程中可能注冊(cè)多個(gè)微任務(wù)(如 Promise.then)
執(zhí)行完宏任務(wù)后,立即依次執(zhí)行所有微任務(wù)
執(zhí)行 UI 更新與渲染
進(jìn)入下一輪循環(huán),重復(fù)上述流程
四、相關(guān)問(wèn)題的解答
1. JS為什么會(huì)阻塞渲染
由于 JS 腳本的執(zhí)行與頁(yè)面渲染共用主線(xiàn)程,若 JS 長(zhǎng)時(shí)間執(zhí)行,渲染任務(wù)就會(huì)延后。
例如以下同步阻塞代碼:
while(Date.now() < performance.now() + 2000) {}
頁(yè)面會(huì)在 2 秒內(nèi)完全無(wú)法響應(yīng)。
此外,瀏覽器為了防止 JS 操作 DOM 導(dǎo)致的視覺(jué)中斷,在 JS 執(zhí)行期間通常會(huì)“暫停渲染”,等腳本執(zhí)行完畢再統(tǒng)一渲染頁(yè)面。
2. JS的計(jì)時(shí)器能做到真正的精確計(jì)時(shí)嘛
(1)計(jì)算機(jī)硬件和操作系統(tǒng)的限制
CPU時(shí)鐘精度問(wèn)題,計(jì)算機(jī)沒(méi)有原子鐘,無(wú)法精確計(jì)算時(shí)間
操作系統(tǒng)提供的計(jì)時(shí) API(如 Windows 的 GetSystemTime
)存在 1-15ms 的調(diào)用誤差。
(2)瀏覽器的引擎機(jī)制
嵌套超限補(bǔ)償,當(dāng)定時(shí)器嵌套層級(jí)超過(guò) 5 層時(shí)(如回調(diào)中再次調(diào)用 setTimeout
),瀏覽器會(huì)強(qiáng)制增加 4ms 延遲:
// 假設(shè)這是第六次調(diào)用,調(diào)用開(kāi)始產(chǎn)生額外延遲,即使設(shè)置延遲為0,也會(huì)有至少4ms延遲 setTimeout(function recur() { setTimeout(recur, 0); // 實(shí)際延遲 ≥4ms }, 0);
最小時(shí)間間隔,即使設(shè)置 setTimeout(fn, 0)
,實(shí)際延遲通常被限制為 1ms(Chrome)或 4ms(舊版瀏覽器)。
(3)事件循環(huán)調(diào)度延遲
主線(xiàn)程阻塞,當(dāng)同步代碼或長(zhǎng)任務(wù)占用主線(xiàn)程時(shí),計(jì)時(shí)器回調(diào)必須等待。
隊(duì)列優(yōu)先級(jí)競(jìng)爭(zhēng),即使準(zhǔn)時(shí)到期,回調(diào)仍需等待:微任務(wù)隊(duì)列清空、更高優(yōu)先級(jí)的交互/動(dòng)畫(huà)任務(wù)、當(dāng)前執(zhí)行棧為空。
總結(jié)
到此這篇關(guān)于JS的運(yùn)行機(jī)制之事件循環(huán)機(jī)制的文章就介紹到這了,更多相關(guān)JS事件循環(huán)機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- JavaScript運(yùn)行機(jī)制之事件循環(huán)(Event Loop)詳解
- 一文詳解JS中的事件循環(huán)機(jī)制
- 關(guān)于js的事件循環(huán)機(jī)制剖析
- 實(shí)例分析js事件循環(huán)機(jī)制
- JS的事件循環(huán)執(zhí)行機(jī)制詳解
- 一文詳解JavaScript中的事件循環(huán)(event?loop)機(jī)制
- 一篇文章帶你了解vue.js的事件循環(huán)機(jī)制
- 詳解JS瀏覽器事件循環(huán)機(jī)制
- 詳解JavaScript事件循環(huán)機(jī)制
- 實(shí)例詳解JS中的事件循環(huán)機(jī)制
相關(guān)文章
JavaScript頁(yè)面實(shí)時(shí)顯示當(dāng)前時(shí)間實(shí)例代碼
最近因?yàn)轫?xiàng)目需要,有個(gè)需求是讓實(shí)時(shí)顯示當(dāng)前時(shí)間,然后想想這不簡(jiǎn)單嗎,自己就動(dòng)手敲代碼,但是發(fā)現(xiàn)一個(gè)問(wèn)題,通過(guò)getMonth()得到月份,總是會(huì)比當(dāng)前月份少1,深深覺(jué)得實(shí)踐出真知啊…之前覺(jué)得Date對(duì)象挺簡(jiǎn)單的,有很多細(xì)節(jié)都沒(méi)有注意。下面這篇文章就給大家詳細(xì)介紹下。2016-10-10手淘flexible.js框架使用和源代碼講解小結(jié)
手淘框架是一個(gè)用來(lái)適配移動(dòng)端的js框架,這篇文章主要介紹了手淘flexible.js框架使用和源代碼講解小結(jié),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-10-10javascript實(shí)現(xiàn)簡(jiǎn)易聊天室
這篇文章主要為大家詳細(xì)介紹了javascript實(shí)現(xiàn)簡(jiǎn)易聊天室,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07通過(guò)babel操作AST精準(zhǔn)插入配置代碼全流程
這篇文章主要為大家介紹了通過(guò)babel操作AST精準(zhǔn)插入配置代碼的全流程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-02-02JavaScript callback回調(diào)函數(shù)用法實(shí)例分析
這篇文章主要介紹了JavaScript callback回調(diào)函數(shù)用法,結(jié)合實(shí)例形式分析了callback回調(diào)函數(shù)的概念、功能、應(yīng)用場(chǎng)景及相關(guān)使用技巧,需要的朋友可以參考下2018-05-05JavaScript中async/await的高級(jí)用法小結(jié)
JavaScript的異步編程已經(jīng)從回調(diào)(Callback)演進(jìn)到Promise,再到如今廣泛使用的async/await語(yǔ)法,本文為大家整理了7個(gè)async/await高級(jí)用法,希望對(duì)大家有所幫助2023-12-12使用post方法實(shí)現(xiàn)json往返傳輸數(shù)據(jù)的方法
今天小編就為大家分享一篇關(guān)于使用post方法實(shí)現(xiàn)json往返傳輸數(shù)據(jù)的方法,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03