一文帶你掌握JavaScript中的EventLoop機制
EventLoop
JavaScript是 單線程、非阻塞 的,它通過事件隊列 (Event Loop) 的方式來實現(xiàn)異步回調(diào)。
關鍵點
- 單線程:JavaScript是單線程的,即同一個時間只能做一件事,事件循環(huán)使其能夠高效地處理異步操作。
- 非阻塞式I/O:事件循環(huán)使得JavaScript可以執(zhí)行長時間運行的任務(如I/O操作),而不會阻塞線程。
- 微任務優(yōu)先:在每個宏任務(Macrotask)執(zhí)行完畢后,立即執(zhí)行當前 微任務隊列 中的所有微任務(依次執(zhí)行),在進行下一個宏任務之前。
為什么 javascript 是單線程的
我們知道多線程可以提高效率啊,為什么JavaScript被設計為單線程,主要原因是它創(chuàng)建的初衷:與用戶的交互以及操作DOM。
在Web頁面中,腳本需要響應用戶的操作:如點擊按鈕、提交表單等,這些操作涉及到對DOM的讀寫。假如JavaScript是多線程的,那么就會引入復雜的同步問題。
例如:兩個線程同時嘗試修改同一個DOM節(jié)點,那么會產(chǎn)生競態(tài)條件,導致不可預測的結(jié)果。所以js一誕生就是單線程并且未來也不會改變
為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創(chuàng)建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準并沒有改變JavaScript單線程的本質(zhì)。
簡化開發(fā)
單線程模型簡化了JavaScript的設計和使用。開發(fā)者可以更專注于實現(xiàn)功能,而不用擔心如線程同步、死鎖等多線程編程中常見的問題。
避免DOM操作沖突
javascript選擇只用一個主線程來執(zhí)行代碼,任何時刻只有一個操作可以被執(zhí)行,從而保證了DOM操作的一致性和可預測性。
事件循環(huán)和異步編程
盡管JavaScript是單線程的,但它通過事件循環(huán)(Event Loop)機制支持異步編程。事件循環(huán)允許JavaScript在執(zhí)行I/O密集型或耗時任務(如Ajax請求、文件操作等)時,不會阻塞主線程。
這是通過將這些任務設置為異步操作并配合回調(diào)函數(shù)、Promise或async/await來實現(xiàn)的。事件循環(huán)和異步編程模型使JavaScript能夠高效地處理多種操作,而無需引入多線程的復雜性。
什么是非阻塞
JavaScript的非阻塞特性是指在執(zhí)行耗時操作(如I/O操作、請求數(shù)據(jù)等)時,不會阻塞程序的其他部分繼續(xù)執(zhí)行。
這種特性是通過異步編程模式實現(xiàn)的,它允許JavaScript在等待某個操作完成的同時,繼續(xù)執(zhí)行代碼的其他部分,從而提高程序的整體性能和響應能力。
為什么需要非阻塞
在瀏覽器環(huán)境中,JavaScript運行在單線程中,這意味著在同一時間內(nèi)只能執(zhí)行一個任務。如果JavaScript執(zhí)行一個長時間運行的任務,如從服務器下載大量數(shù)據(jù),它將阻塞后續(xù)代碼的執(zhí)行,導致整個頁面無法響應用戶操作,甚至出現(xiàn)卡頓。
通過非阻塞異步編程,JavaScript可以在等待某個長時間運行的任務完成的同時,繼續(xù)執(zhí)行其他任務,提高應用的響應性和用戶體驗。
實現(xiàn)非阻塞的方式有哪些?
1) 回調(diào)函數(shù)(Callbacks)
最初,JavaScript通過回調(diào)函數(shù)實現(xiàn)非阻塞行為。當一個異步操作開始時,會傳入一個函數(shù)(回調(diào)函數(shù)),這個函數(shù)會在異步操作完成時被調(diào)用。
這種方式雖然解決了非阻塞的問題,但當有多個異步操作需要協(xié)同工作時,會導致所謂的“回調(diào)地獄”(Callback Hell),使得代碼難以閱讀和維護。
2) Promise
為了解決回調(diào)函數(shù)帶來的問題,ES6引入了Promise對象。Promise提供了一種更優(yōu)雅的方式來處理異步操作。
它代表了一個異步操作的最終完成(或失敗)及其結(jié)果值。通過.then()和.catch()方法,可以更容易地組織和管理異步操作及其結(jié)果。
3)async/await
ES7進一步引入了async和await關鍵字,使得使用Promise的代碼可以像寫同步代碼那樣簡潔明了。
async函數(shù)聲明一個函數(shù)是異步的,而await關鍵字用于等待一個Promise解決(resolve)。使用async和await可以以同步的方式寫出清晰、易讀的代碼,同時保持異步操作的非阻塞特性。
同步任務、異步任務、宏任務、微任務之間的概念和關系
JS 分為同步任務和異步任務;同步任務都在JS引擎線程上執(zhí)行,形成一個執(zhí)行棧,它們與事件循環(huán)無關;異步任務被分為宏任務和微任務,它們都依賴事件循環(huán)進行調(diào)度,但執(zhí)行時機不同;
事件觸發(fā)線程管理一個任務隊列,異步任務觸發(fā)條件達成,將回調(diào)事件放到任務隊列中。
同步任務:這些任務在主線程上按順序執(zhí)行,執(zhí)行過程會阻塞后續(xù)任務的執(zhí)行,直到當前任務完成。同步任務的執(zhí)行不依賴事件循環(huán),它們直接在調(diào)用棧(執(zhí)行棧)中按順序執(zhí)行。
異步任務:異步任務的執(zhí)行不會立即完成,它們依賴事件循環(huán)進行調(diào)度。異步任務包括宏任務和微任務,它們在特定的時間點被推入各自的隊列中等待執(zhí)行。
宏任務(Macrotasks)
宏任務是一類異步任務,它們代表了一些較大的、獨立的工作單元。每個宏任務的執(zhí)行會在一個新的事件循環(huán)中進行,包括:setTimeout、setInterval、I/O 操作、UI 渲染(在瀏覽器環(huán)境中)、postMessage等
微任務(Microtasks)
微任務也是異步任務,但它們用于處理一些需要盡快執(zhí)行的較小的工作單元。**微任務在當前宏任務執(zhí)行完畢后、下一個宏任務開始之前執(zhí)行。包括:Promise(??promise本身是同步的,回調(diào)函數(shù)才是異步的)的回調(diào)(.then、.catch、.finally)、MutationObserver的回調(diào)、queueMicrotask。
一次事件循環(huán)的執(zhí)行
- 執(zhí)行當前宏任務:事件循環(huán)首先從宏任務隊列(也稱為任務隊列)中取出第一個任務執(zhí)行。這包括了諸如
setTimeout、setInterval、I/O 操作、用戶交互事件(如點擊或鍵盤事件)等任務。 - 執(zhí)行所有微任務:當前宏任務執(zhí)行完畢后,事件循環(huán)會檢查微任務隊列。如果隊列中有微任務(例如,由
Promise.then()或MutationObserver等產(chǎn)生的任務),事件循環(huán)會依次執(zhí)行隊列中的所有微任務,直到微任務隊列清空。微任務的執(zhí)行是連續(xù)的,不會中斷。 - 渲染UI(如果需要):在瀏覽器環(huán)境中,一旦微任務隊列清空,瀏覽器會檢查是否需要執(zhí)行UI渲染。通常,瀏覽器的UI渲染會在執(zhí)行完所有微任務之后,下一個宏任務開始之前進行。
- 繼續(xù)下一個宏任務:完成當前宏任務、所有微任務以及可能的UI渲染之后,事件循環(huán)會回到第一步,從宏任務隊列中取出下一個任務,開始新一輪的執(zhí)行。
在同一次事件循環(huán)中,微任務(Microtasks)總是在當前宏任務(Macrotasks)之后執(zhí)行。

上圖中,主線程運行的時候,產(chǎn)生堆(heap)和棧(stack),棧中的代碼調(diào)用各種外部API,它們在"任務隊列"中加入各種事件(click,load,done)。
只要棧中的代碼執(zhí)行完畢,主線程就會去讀取"任務隊列",依次執(zhí)行那些事件所對應的回調(diào)函數(shù)。
實踐
setTimeout(() => {
console.log(1)
}, 0)
console.log(2)
const promise2 = new Promise((resolve) => {
console.log(3)
resolve(3)
})
promise2.then((res) => {
console.log(4)
})
執(zhí)行步驟和輸出結(jié)果的解釋:
- setTimeout(() => { console.log(1) }, 0) 注冊了一個宏任務,但它的回調(diào)函數(shù)(打印
1)不會立即執(zhí)行。放入宏任務隊列中,等待當前執(zhí)行棧清空和所有微任務完成后再執(zhí)行。 - console.log(2) 是同步代碼,會立即執(zhí)行,輸出
2。 - 創(chuàng)建了一個新的Promise對象promise2,*console.log(3)*也是同步代碼,因此會立即執(zhí)行,輸出
3。 - 在Promise的executor函數(shù)中,調(diào)用resolve(3)。這會將 promise2.then((res) => { console.log(4) }) 中的回調(diào)函數(shù)加入到微任務隊列,等待當前執(zhí)行棧清空后執(zhí)行。
- 此時,同步代碼執(zhí)行完畢。
- 在進入下一個宏任務之前,事件循環(huán)會處理所有微任務。因此,promise2.then()的回調(diào)函數(shù)會被執(zhí)行,輸出
4。 - 最后,事件循環(huán)處理下一個宏任務,即 setTimeout() 的回調(diào)函數(shù),執(zhí)行并輸出
1。
所以,輸出結(jié)果為:2、3、4、1。
js整體執(zhí)行順序是:同步任務 -> 微任務 -> 宏任務 -> 微任務 -> 宏任務 -> ...
總結(jié)
- 執(zhí)行一個
宏任務(棧中沒有就從事件隊列中獲?。?/li> - 執(zhí)行過程中如果遇到
微任務,就將它添加到微任務的任務隊列中 宏任務執(zhí)行完畢后,立即執(zhí)行當前微任務隊列中的所有微任務(依次執(zhí)行)- 當前
宏任務執(zhí)行完畢,開始檢查渲染,然后GUI線程接管渲染 - 渲染完畢后,
JS線程繼續(xù)接管,開始下一個宏任務(從事件隊列中獲取)

分享一個實用工具:Event Loop可視化面板
到此這篇關于一文帶你掌握JavaScript中的EventLoop機制的文章就介紹到這了,更多相關JavaScript EventLoop機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
javascript 刪除select中的所有option的實例
這篇文章主要介紹了javascript 刪除select中的所有option的實例的相關資料,希望通過本文能幫助到大家實現(xiàn)這樣的功能,需要的朋友可以參考下2017-09-09
webpack 自動清理 dist 文件夾的兩種實現(xiàn)方法
這篇文章主要介紹了webpack 自動清理 dist 文件夾的兩種實現(xiàn)方法,本文給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧2024-06-06
JavaScript結(jié)合Bootstrap仿微信后臺多圖文界面管理
這篇文章主要為大家詳細介紹了js結(jié)合Bootstrap仿微信后臺多圖文界面管理,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-07-07
基于rollup的組件庫打包體積優(yōu)化小結(jié)
這篇文章主要介紹了基于rollup的組件庫打包體積優(yōu)化小結(jié),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06

