一文帶你掌握J(rèn)avaScript中的EventLoop機(jī)制
EventLoop
JavaScript是 單線程
、非阻塞
的,它通過事件隊(duì)列 (Event Loop)
的方式來實(shí)現(xiàn)異步回調(diào)。
關(guān)鍵點(diǎn)
- 單線程:JavaScript是單線程的,即同一個(gè)時(shí)間只能做一件事,事件循環(huán)使其能夠高效地處理異步操作。
- 非阻塞式I/O:事件循環(huán)使得JavaScript可以執(zhí)行長(zhǎng)時(shí)間運(yùn)行的任務(wù)(如I/O操作),而不會(huì)阻塞線程。
- 微任務(wù)優(yōu)先:在每個(gè)宏任務(wù)(Macrotask)執(zhí)行完畢后,立即執(zhí)行當(dāng)前 微任務(wù)隊(duì)列 中的所有微任務(wù)(依次執(zhí)行),在進(jìn)行下一個(gè)宏任務(wù)之前。
為什么 javascript 是單線程的
我們知道多線程可以提高效率啊,為什么JavaScript被設(shè)計(jì)為單線程,主要原因是它創(chuàng)建的初衷:與用戶的交互以及操作DOM。
在Web頁面中,腳本需要響應(yīng)用戶的操作:如點(diǎn)擊按鈕、提交表單等,這些操作涉及到對(duì)DOM的讀寫。假如JavaScript是多線程的,那么就會(huì)引入復(fù)雜的同步問題。
例如:兩個(gè)線程同時(shí)嘗試修改同一個(gè)DOM節(jié)點(diǎn),那么會(huì)產(chǎn)生競(jìng)態(tài)條件,導(dǎo)致不可預(yù)測(cè)的結(jié)果。所以js一誕生就是單線程并且未來也不會(huì)改變
為了利用多核CPU的計(jì)算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個(gè)新標(biāo)準(zhǔn)并沒有改變JavaScript單線程的本質(zhì)。
簡(jiǎn)化開發(fā)
單線程模型簡(jiǎn)化了JavaScript的設(shè)計(jì)和使用。開發(fā)者可以更專注于實(shí)現(xiàn)功能,而不用擔(dān)心如線程同步、死鎖等多線程編程中常見的問題。
避免DOM操作沖突
javascript選擇只用一個(gè)主線程來執(zhí)行代碼,任何時(shí)刻只有一個(gè)操作可以被執(zhí)行,從而保證了DOM操作的一致性和可預(yù)測(cè)性。
事件循環(huán)和異步編程
盡管JavaScript是單線程的,但它通過事件循環(huán)(Event Loop)機(jī)制支持異步編程。事件循環(huán)允許JavaScript在執(zhí)行I/O密集型或耗時(shí)任務(wù)(如Ajax請(qǐng)求、文件操作等)時(shí),不會(huì)阻塞主線程。
這是通過將這些任務(wù)設(shè)置為異步操作并配合回調(diào)函數(shù)、Promise或async/await來實(shí)現(xiàn)的。事件循環(huán)和異步編程模型使JavaScript能夠高效地處理多種操作,而無需引入多線程的復(fù)雜性。
什么是非阻塞
JavaScript的非阻塞特性是指在執(zhí)行耗時(shí)操作(如I/O操作、請(qǐng)求數(shù)據(jù)等)時(shí),不會(huì)阻塞程序的其他部分繼續(xù)執(zhí)行。
這種特性是通過異步編程模式實(shí)現(xiàn)的,它允許JavaScript在等待某個(gè)操作完成的同時(shí),繼續(xù)執(zhí)行代碼的其他部分,從而提高程序的整體性能和響應(yīng)能力。
為什么需要非阻塞
在瀏覽器環(huán)境中,JavaScript運(yùn)行在單線程中,這意味著在同一時(shí)間內(nèi)只能執(zhí)行一個(gè)任務(wù)。如果JavaScript執(zhí)行一個(gè)長(zhǎng)時(shí)間運(yùn)行的任務(wù),如從服務(wù)器下載大量數(shù)據(jù),它將阻塞后續(xù)代碼的執(zhí)行,導(dǎo)致整個(gè)頁面無法響應(yīng)用戶操作,甚至出現(xiàn)卡頓。
通過非阻塞異步編程,JavaScript可以在等待某個(gè)長(zhǎng)時(shí)間運(yùn)行的任務(wù)完成的同時(shí),繼續(xù)執(zhí)行其他任務(wù),提高應(yīng)用的響應(yīng)性和用戶體驗(yàn)。
實(shí)現(xiàn)非阻塞的方式有哪些?
1) 回調(diào)函數(shù)(Callbacks)
最初,JavaScript通過回調(diào)函數(shù)實(shí)現(xiàn)非阻塞行為。當(dāng)一個(gè)異步操作開始時(shí),會(huì)傳入一個(gè)函數(shù)(回調(diào)函數(shù)),這個(gè)函數(shù)會(huì)在異步操作完成時(shí)被調(diào)用。
這種方式雖然解決了非阻塞的問題,但當(dāng)有多個(gè)異步操作需要協(xié)同工作時(shí),會(huì)導(dǎo)致所謂的“回調(diào)地獄”(Callback Hell),使得代碼難以閱讀和維護(hù)。
2) Promise
為了解決回調(diào)函數(shù)帶來的問題,ES6引入了Promise對(duì)象。Promise提供了一種更優(yōu)雅的方式來處理異步操作。
它代表了一個(gè)異步操作的最終完成(或失敗)及其結(jié)果值。通過.then()和.catch()方法,可以更容易地組織和管理異步操作及其結(jié)果。
3)async/await
ES7進(jìn)一步引入了async
和await
關(guān)鍵字,使得使用Promise的代碼可以像寫同步代碼那樣簡(jiǎn)潔明了。
async
函數(shù)聲明一個(gè)函數(shù)是異步的,而await
關(guān)鍵字用于等待一個(gè)Promise解決(resolve)。使用async
和await
可以以同步的方式寫出清晰、易讀的代碼,同時(shí)保持異步操作的非阻塞特性。
同步任務(wù)、異步任務(wù)、宏任務(wù)、微任務(wù)之間的概念和關(guān)系
JS 分為同步任務(wù)和異步任務(wù);同步任務(wù)都在JS引擎線程上執(zhí)行,形成一個(gè)執(zhí)行棧,它們與事件循環(huán)無關(guān);異步任務(wù)被分為宏任務(wù)和微任務(wù),它們都依賴事件循環(huán)進(jìn)行調(diào)度,但執(zhí)行時(shí)機(jī)不同;
事件觸發(fā)線程管理一個(gè)任務(wù)隊(duì)列,異步任務(wù)觸發(fā)條件達(dá)成,將回調(diào)事件放到任務(wù)隊(duì)列中。
同步任務(wù):這些任務(wù)在主線程上按順序執(zhí)行,執(zhí)行過程會(huì)阻塞后續(xù)任務(wù)的執(zhí)行,直到當(dāng)前任務(wù)完成。同步任務(wù)的執(zhí)行不依賴事件循環(huán),它們直接在調(diào)用棧(執(zhí)行棧)中按順序執(zhí)行。
異步任務(wù):異步任務(wù)的執(zhí)行不會(huì)立即完成,它們依賴事件循環(huán)進(jìn)行調(diào)度。異步任務(wù)包括宏任務(wù)和微任務(wù),它們?cè)谔囟ǖ臅r(shí)間點(diǎn)被推入各自的隊(duì)列中等待執(zhí)行。
宏任務(wù)(Macrotasks)
宏任務(wù)是一類異步任務(wù),它們代表了一些較大的、獨(dú)立的工作單元。每個(gè)宏任務(wù)的執(zhí)行會(huì)在一個(gè)新的事件循環(huán)中進(jìn)行,包括:setTimeout
、setInterval
、I/O 操作
、UI 渲染
(在瀏覽器環(huán)境中)、postMessage
等
微任務(wù)(Microtasks)
微任務(wù)也是異步任務(wù),但它們用于處理一些需要盡快執(zhí)行的較小的工作單元。**微任務(wù)在當(dāng)前宏任務(wù)執(zhí)行完畢后、下一個(gè)宏任務(wù)開始之前執(zhí)行。包括:Promise(??promise本身是同步的,回調(diào)函數(shù)才是異步的)的回調(diào)(.then
、.catch
、.finally
)、MutationObserver
的回調(diào)、queueMicrotask
。
一次事件循環(huán)的執(zhí)行
- 執(zhí)行當(dāng)前宏任務(wù):事件循環(huán)首先從宏任務(wù)隊(duì)列(也稱為任務(wù)隊(duì)列)中取出第一個(gè)任務(wù)執(zhí)行。這包括了諸如
setTimeout
、setInterval
、I/O 操作、用戶交互事件(如點(diǎn)擊或鍵盤事件)等任務(wù)。 - 執(zhí)行所有微任務(wù):當(dāng)前宏任務(wù)執(zhí)行完畢后,事件循環(huán)會(huì)檢查微任務(wù)隊(duì)列。如果隊(duì)列中有微任務(wù)(例如,由
Promise.then()
或MutationObserver
等產(chǎn)生的任務(wù)),事件循環(huán)會(huì)依次執(zhí)行隊(duì)列中的所有微任務(wù),直到微任務(wù)隊(duì)列清空。微任務(wù)的執(zhí)行是連續(xù)的,不會(huì)中斷。 - 渲染UI(如果需要):在瀏覽器環(huán)境中,一旦微任務(wù)隊(duì)列清空,瀏覽器會(huì)檢查是否需要執(zhí)行UI渲染。通常,瀏覽器的UI渲染會(huì)在執(zhí)行完所有微任務(wù)之后,下一個(gè)宏任務(wù)開始之前進(jìn)行。
- 繼續(xù)下一個(gè)宏任務(wù):完成當(dāng)前宏任務(wù)、所有微任務(wù)以及可能的UI渲染之后,事件循環(huán)會(huì)回到第一步,從宏任務(wù)隊(duì)列中取出下一個(gè)任務(wù),開始新一輪的執(zhí)行。
在同一次事件循環(huán)中,微任務(wù)(Microtasks)總是在當(dāng)前宏任務(wù)(Macrotasks)之后執(zhí)行。
上圖中,主線程運(yùn)行的時(shí)候,產(chǎn)生堆(heap)和棧(stack),棧中的代碼調(diào)用各種外部API,它們?cè)?quot;任務(wù)隊(duì)列"中加入各種事件(click,load,done)。
只要棧中的代碼執(zhí)行完畢,主線程就會(huì)去讀取"任務(wù)隊(duì)列",依次執(zhí)行那些事件所對(duì)應(yīng)的回調(diào)函數(shù)。
實(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) 注冊(cè)了一個(gè)宏任務(wù),但它的回調(diào)函數(shù)(打印
1
)不會(huì)立即執(zhí)行。放入宏任務(wù)隊(duì)列中,等待當(dāng)前執(zhí)行棧清空和所有微任務(wù)完成后再執(zhí)行。 - console.log(2) 是同步代碼,會(huì)立即執(zhí)行,輸出
2
。 - 創(chuàng)建了一個(gè)新的Promise對(duì)象promise2,*console.log(3)*也是同步代碼,因此會(huì)立即執(zhí)行,輸出
3
。 - 在Promise的executor函數(shù)中,調(diào)用resolve(3)。這會(huì)將 promise2.then((res) => { console.log(4) }) 中的回調(diào)函數(shù)加入到微任務(wù)隊(duì)列,等待當(dāng)前執(zhí)行棧清空后執(zhí)行。
- 此時(shí),同步代碼執(zhí)行完畢。
- 在進(jìn)入下一個(gè)宏任務(wù)之前,事件循環(huán)會(huì)處理所有微任務(wù)。因此,promise2.then()的回調(diào)函數(shù)會(huì)被執(zhí)行,輸出
4
。 - 最后,事件循環(huán)處理下一個(gè)宏任務(wù),即 setTimeout() 的回調(diào)函數(shù),執(zhí)行并輸出
1
。
所以,輸出結(jié)果為:2
、3
、4
、1
。
js整體執(zhí)行順序是:同步任務(wù) -> 微任務(wù) -> 宏任務(wù) -> 微任務(wù) -> 宏任務(wù) -> ...
總結(jié)
- 執(zhí)行一個(gè)
宏任務(wù)
(棧中沒有就從事件隊(duì)列
中獲?。?/li> - 執(zhí)行過程中如果遇到
微任務(wù)
,就將它添加到微任務(wù)
的任務(wù)隊(duì)列中 宏任務(wù)
執(zhí)行完畢后,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列
中的所有微任務(wù)
(依次執(zhí)行)- 當(dāng)前
宏任務(wù)
執(zhí)行完畢,開始檢查渲染,然后GUI線程
接管渲染 - 渲染完畢后,
JS線程
繼續(xù)接管,開始下一個(gè)宏任務(wù)
(從事件隊(duì)列中獲?。?/li>
分享一個(gè)實(shí)用工具:Event Loop可視化面板
到此這篇關(guān)于一文帶你掌握J(rèn)avaScript中的EventLoop機(jī)制的文章就介紹到這了,更多相關(guān)JavaScript EventLoop機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
帶你領(lǐng)略O(shè)bject.assign()方法的操作方式
這篇文章主要介紹了帶你領(lǐng)略O(shè)bject.assign()方法的操作方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08一文帶你搞懂JavaScript中轉(zhuǎn)義字符的使用
說起轉(zhuǎn)義字符,大家最先想到的肯定是使用反斜杠,這也是我們最常見的,很多編程語言都支持。除了反斜杠以外,在前端開發(fā)中,還有其他幾種轉(zhuǎn)義字符,也是較常見的,本文將對(duì)這些做一個(gè)總結(jié)2023-02-02JavaScript搜索字符串并將搜索結(jié)果返回到字符串的方法
這篇文章主要介紹了JavaScript搜索字符串并將搜索結(jié)果返回到字符串的方法,涉及javascript中match方法操作字符串的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-04-04setTimeout自動(dòng)觸發(fā)一個(gè)js的方法
本文為大家介紹下使用setTimeout自動(dòng)觸發(fā)一個(gè)js,具體實(shí)現(xiàn)如下,喜歡的朋友可以學(xué)習(xí)下2014-01-01(推薦一個(gè)超好的JS函數(shù)庫)S.Sams Lifexperience ScriptClassLib
(推薦一個(gè)超好的JS函數(shù)庫)S.Sams Lifexperience ScriptClassLib...2007-04-04js代碼運(yùn)行報(bào)錯(cuò)Warning:To?load?an?ES?module,?set?"type&q
最近在學(xué)習(xí)ES6的過程中,和運(yùn)行javascript文件時(shí)進(jìn)行了報(bào)錯(cuò),下面這篇文章主要給大家介紹了關(guān)于js代碼運(yùn)行報(bào)錯(cuò)Warning:To?load?an?ES?module,?set?"type":"module"in?the?package.json?or?use?the?.mjs的相關(guān)資料,需要的朋友可以參考下2023-04-04