前端學(xué)習(xí)17 事件循環(huán)(Event Loop)機(jī)制
JavaScript作為一門(mén)單線(xiàn)程語(yǔ)言,卻能夠高效處理異步任務(wù),這一特性使其在處理網(wǎng)絡(luò)請(qǐng)求、用戶(hù)交互等場(chǎng)景下表現(xiàn)出色。而這一切的背后,都離不開(kāi)Event Loop(事件循環(huán))這一核心機(jī)制。
1.JavaScript的單線(xiàn)程
JavaScript是一門(mén)單線(xiàn)程語(yǔ)言,這意味著它只有一個(gè)主線(xiàn)程用于執(zhí)行代碼。這一設(shè)計(jì)決策主要是為了簡(jiǎn)化DOM操作的復(fù)雜性,避免多線(xiàn)程可能導(dǎo)致的并發(fā)問(wèn)題。然而,單線(xiàn)程也帶來(lái)了一個(gè)明顯的問(wèn)題:如果某個(gè)任務(wù)執(zhí)行時(shí)間過(guò)長(zhǎng),會(huì)導(dǎo)致整個(gè)應(yīng)用"卡住",無(wú)法響應(yīng)用戶(hù)交互。
為了解決這個(gè)問(wèn)題,JavaScript引入了異步編程模型,而Event Loop就是這個(gè)模型的核心機(jī)制。
1.1 核心組件
JavaScript運(yùn)行時(shí)環(huán)境主要由以下幾個(gè)部分組成:
- 調(diào)用棧(Call Stack):用于追蹤函數(shù)調(diào)用的棧結(jié)構(gòu),記錄當(dāng)前執(zhí)行的代碼位置。
- 堆(Heap):用于存儲(chǔ)對(duì)象、數(shù)組等復(fù)雜的數(shù)據(jù)機(jī)構(gòu)的內(nèi)存區(qū)域
- 任務(wù)隊(duì)列(Task Queues):存儲(chǔ)待執(zhí)行的回調(diào)函數(shù) 宏任務(wù)隊(duì)列和微任務(wù)隊(duì)列
- Web API/Node API:由運(yùn)行環(huán)境(瀏覽器/Node.js)提供的API,如定時(shí)器、網(wǎng)絡(luò)請(qǐng)求等
而是事件循環(huán)(Event Loop):協(xié)調(diào)調(diào)用棧和任務(wù)隊(duì)列之間的關(guān)系
1.2 調(diào)用棧
調(diào)用棧是一種LIFO(后進(jìn)先出)的數(shù)據(jù)結(jié)構(gòu),用于跟蹤代碼的執(zhí)行位置。當(dāng)調(diào)用一個(gè)函數(shù)時(shí),會(huì)將其壓入棧頂;當(dāng)函數(shù)執(zhí)行完畢,會(huì)從棧頂彈出。
function multiply(a, b) { return a * b; } function square(n) { return multiply(n, n); } function printSquare(n) { const result = square(n); console.log(result); } printSquare(5);
- 將printSquare(5)壓入棧
- 在printSquare內(nèi)部,將square(5)壓入棧
- 在square內(nèi)部,將multiply(5, 5)壓入棧
- multiply計(jì)算結(jié)果并返回,從棧中彈出
- square獲得結(jié)果并返回,從棧中彈出
- printSquare打印結(jié)果并結(jié)束,從棧中彈出
此時(shí),調(diào)用棧為空,標(biāo)志著同步代碼執(zhí)行完畢。
2. 事件循環(huán)核心機(jī)制
2.1 事件循環(huán)流程
- 執(zhí)行同步代碼,這些代碼會(huì)立即進(jìn)入調(diào)用棧執(zhí)行
- 調(diào)用棧清空后,檢查微任務(wù)隊(duì)列,依次執(zhí)行所有微任務(wù)
- 微任務(wù)隊(duì)列清空后,取出一個(gè)宏任務(wù)執(zhí)行
- 宏任務(wù)執(zhí)行完畢后,再次檢查微任務(wù)隊(duì)列,執(zhí)行所有微任務(wù)
- 重復(fù)步驟3和4,形成一個(gè)循環(huán)
2.2 宏任務(wù)(Macrotask)和微任務(wù)(Microtask)
理解宏任務(wù)(Macrotask)和微任務(wù)(Microtask)的區(qū)別是掌握事件循環(huán)的關(guān)鍵。
宏任務(wù)(Macrotask)包括:
- setTimeout和setInterval回調(diào)
- setImmediate回調(diào)(Node.js環(huán)境)
- I/O操作回調(diào)
- UI交互事件
- requestAnimationFrame(瀏覽器環(huán)境)
- MessageChannel回調(diào)
微任務(wù)(Microtask)包括:
- Promise的
then
、catch
和finally
回調(diào) queueMicrotask
回調(diào)MutationObserver
回調(diào)(瀏覽器環(huán)境)process.nextTick
回調(diào)(Node.js環(huán)境,優(yōu)先級(jí)高于其他微任務(wù))
微任務(wù)優(yōu)先級(jí)高于宏任務(wù),即當(dāng)前宏任務(wù)執(zhí)行完后,會(huì)先清空微任務(wù)隊(duì)列,再執(zhí)行下一個(gè)宏任務(wù)。
console.log('1. 同步代碼開(kāi)始'); setTimeout(() => { console.log('2. 宏任務(wù)(setTimeout回調(diào))'); new Promise(resolve => { console.log('3. 宏任務(wù)中的同步代碼'); resolve(); }).then(() => { console.log('4. 宏任務(wù)中的微任務(wù)'); }); }, 0); new Promise(resolve => { console.log('5. 同步代碼中的Promise'); resolve(); }).then(() => { console.log('6. 微任務(wù)'); }); console.log('7. 同步代碼結(jié)束');
輸出順序?yàn)椋?/p>
- 1. 同步代碼開(kāi)始
- 5. 同步代碼中的Promise
- 7. 同步代碼結(jié)束
- 6. 微任務(wù)
- 2. 宏任務(wù)(setTimeout回調(diào))
- 3. 宏任務(wù)中的同步代碼
- 4. 宏任務(wù)中的微任務(wù)
執(zhí)行過(guò)程分析:
- 首先執(zhí)行同步代碼,輸出"1. 同步代碼開(kāi)始"
- 遇到setTimeout,將其回調(diào)放入宏任務(wù)隊(duì)列
- 遇到Promise構(gòu)造函數(shù),其內(nèi)部代碼是同步執(zhí)行的,輸出"5. 同步代碼中的Promise"
- 將Promise的then回調(diào)放入微任務(wù)隊(duì)列
- 輸出"7. 同步代碼結(jié)束"
- 同步代碼執(zhí)行完畢,檢查微任務(wù)隊(duì)列,執(zhí)行Promise的then回調(diào),輸出"6. 微任務(wù)"
- 微任務(wù)隊(duì)列清空,從宏任務(wù)隊(duì)列取出setTimeout回調(diào)執(zhí)行
- 在setTimeout回調(diào)中,輸出"2. 宏任務(wù)(setTimeout回調(diào))"
- 遇到新的Promise,輸出"3. 宏任務(wù)中的同步代碼"
- 將新Promise的then回調(diào)放入微任務(wù)隊(duì)列
- setTimeout回調(diào)執(zhí)行完畢,檢查微任務(wù)隊(duì)列,執(zhí)行Promise的then回調(diào),輸出"4. 宏任務(wù)中的微任務(wù)"
3.瀏覽器的事件循環(huán)
瀏覽器環(huán)境中的事件循環(huán)有其獨(dú)特的特性,特別是與渲染管道的交互。
在瀏覽器環(huán)境中,渲染步驟(樣式計(jì)算、布局、繪制等)通常發(fā)生在宏任務(wù)之間,且在所有微任務(wù)執(zhí)行完畢之后。這意味著,如果你想在下一次渲染前操作DOM,應(yīng)該使用微任務(wù)或requestAnimationFrame(用于在下一次瀏覽器重繪之前執(zhí)行指定的回調(diào)函數(shù)。它的作用是告訴瀏覽器我們希望執(zhí)行一段動(dòng)畫(huà),并在動(dòng)畫(huà)執(zhí)行時(shí)進(jìn)行優(yōu)化,以獲得更流暢的效果)。
// 不建議的寫(xiě)法:可能導(dǎo)致多次不必要的重排 button.addEventListener('click', () => { box.style.width = '100px'; box.style.height = '100px'; box.style.margin = '20px'; }); // 優(yōu)化的寫(xiě)法:所有DOM操作合并到下一幀執(zhí)行 button.addEventListener('click', () => { requestAnimationFrame(() => { box.style.width = '100px'; box.style.height = '100px'; box.style.margin = '20px'; }); });
瀏覽器環(huán)境中的setTimeout和setInterval并不保證在指定時(shí)間后精確執(zhí)行,只能保證在指定時(shí)間后將回調(diào)膠乳宏任務(wù)隊(duì)列。如果調(diào)用?;蚱渌耆蝿?wù)占用主線(xiàn)程,定時(shí)器回調(diào)會(huì)被延遲執(zhí)行。
此外,大多數(shù)瀏覽器對(duì)不活躍標(biāo)簽頁(yè)中的定時(shí)器有最小間隔限制(通常為1000ms),以節(jié)省系統(tǒng)資源。
4.事件循環(huán)與異步模式
4.1 回調(diào)地獄
// 回調(diào)地獄示例 getData(function(a) { getMoreData(a, function(b) { getEvenMoreData(b, function(c) { getFinalData(c, function(result) { console.log(result); }, handleError); }, handleError); }, handleError); }, handleError);
解決方案:
1、使用Promise鏈:將嵌套回調(diào)轉(zhuǎn)換為扁平的鏈?zhǔn)秸{(diào)用
getData() .then(a => getMoreData(a)) .then(b => getEvenMoreData(b)) .then(c => getFinalData(c)) .then(result => console.log(result)) .catch(handleError);
2、使用async/await:使異步代碼看起來(lái)像同步代碼
async function fetchAllData() { try { const a = await getData(); const b = await getMoreData(a); const c = await getEvenMoreData(b); const result = await getFinalData(c); console.log(result); } catch (error) { handleError(error); } } fetchAllData();