JavaScript中的事件循環(huán)機(jī)制解讀
1. 事件循環(huán)基本概念
1.1 什么是事件循環(huán)?
事件循環(huán)是 JavaScript 處理異步任務(wù)的核心機(jī)制,它讓單線程的 JavaScript 能夠"同時(shí)"處理多個(gè)任務(wù)。
console.log('1. 開(kāi)始');
setTimeout(() => {
console.log('2. 定時(shí)器');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise');
});
console.log('4. 結(jié)束');
// 執(zhí)行順序:1 → 4 → 3 → 2
// 這就是事件循環(huán)在起作用!
1.2 為什么需要事件循環(huán)?
JavaScript 是單線程的,如果沒(méi)有事件循環(huán),遇到耗時(shí)任務(wù)時(shí)頁(yè)面就會(huì)卡死。
// 如果沒(méi)有事件循環(huán) - 糟糕的情況
console.log('開(kāi)始');
const result = synchronousNetworkRequest(); // 假設(shè)這個(gè)請(qǐng)求需要3秒
console.log('收到結(jié)果:', result);
console.log('頁(yè)面卡住了3秒!用戶無(wú)法操作!');
// 有事件循環(huán) - 良好的體驗(yàn)
console.log('開(kāi)始');
asynchronousNetworkRequest((result) => {
console.log('收到結(jié)果:', result);
});
console.log('頁(yè)面可以立即響應(yīng)!用戶可以正常操作!');
2. 事件循環(huán)的組成部分
2.1 三大核心組件
// 可視化事件循環(huán)結(jié)構(gòu)
┌───────────────────────────┐
│ 調(diào)用棧 (Call Stack) │ ← 正在執(zhí)行的代碼
└───────────────────────────┘
↓
┌───────────────────────────┐
│ Web APIs 環(huán)境 │ ← 瀏覽器提供的異步API
│ - setTimeout │
│ - DOM事件 │
│ - 網(wǎng)絡(luò)請(qǐng)求 │
└───────────────────────────┘
↓
┌───────────────────────────┐
│ 任務(wù)隊(duì)列 (Task Queue) │ ← 等待執(zhí)行的回調(diào)函數(shù)
│ 1. 宏任務(wù)隊(duì)列 │
│ 2. 微任務(wù)隊(duì)列 │
└───────────────────────────┘
↓
┌───────────────────────────┐
│ 事件循環(huán) (Event Loop) │ ← 協(xié)調(diào)調(diào)度的"管理員"
└───────────────────────────┘
2.2 調(diào)用棧 (Call Stack)
function first() {
console.log('第一個(gè)函數(shù)開(kāi)始');
second();
console.log('第一個(gè)函數(shù)結(jié)束');
}
function second() {
console.log('第二個(gè)函數(shù)開(kāi)始');
third();
console.log('第二個(gè)函數(shù)結(jié)束');
}
function third() {
console.log('第三個(gè)函數(shù)');
}
first();
// 調(diào)用棧變化:
// 1. first() 入棧
// 2. console.log() 入棧 → 執(zhí)行 → 出棧
// 3. second() 入棧
// 4. console.log() 入棧 → 執(zhí)行 → 出棧
// 5. third() 入棧
// 6. console.log() 入棧 → 執(zhí)行 → 出棧
// 7. third() 出棧
// 8. console.log() 入棧 → 執(zhí)行 → 出棧
// 9. second() 出棧
// 10. first() 出棧
3. 任務(wù)隊(duì)列詳解
3.1 微任務(wù) (Microtasks) vs 宏任務(wù) (Macrotasks)
console.log('腳本開(kāi)始'); // 同步任務(wù)
// 宏任務(wù)
setTimeout(() => {
console.log('setTimeout - 宏任務(wù)');
}, 0);
// 微任務(wù)
Promise.resolve().then(() => {
console.log('Promise - 微任務(wù)');
});
// 另一個(gè)微任務(wù)
queueMicrotask(() => {
console.log('queueMicrotask - 微任務(wù)');
});
console.log('腳本結(jié)束'); // 同步任務(wù)
// 執(zhí)行順序:
// 1. 腳本開(kāi)始 (同步)
// 2. 腳本結(jié)束 (同步)
// 3. Promise - 微任務(wù)
// 4. queueMicrotask - 微任務(wù)
// 5. setTimeout - 宏任務(wù)
3.2 微任務(wù)有哪些?
// 常見(jiàn)的微任務(wù)來(lái)源: Promise.then() / .catch() / .finally() queueMicrotask() MutationObserver(DOM變化觀察) process.nextTick(Node.js) // 微任務(wù)特點(diǎn):優(yōu)先級(jí)高,在每個(gè)宏任務(wù)之后立即執(zhí)行
3.3 宏任務(wù)有哪些?
// 常見(jiàn)的宏任務(wù)來(lái)源: setTimeout / setInterval setImmediate(Node.js) I/O 操作(文件讀取、網(wǎng)絡(luò)請(qǐng)求) UI 渲染(瀏覽器) DOM 事件(click、load等) // 宏任務(wù)特點(diǎn):優(yōu)先級(jí)較低,等待調(diào)用棧清空后執(zhí)行
4. 完整的事件循環(huán)流程
4.1 詳細(xì)執(zhí)行步驟
// 步驟演示
console.log('1. 同步任務(wù)開(kāi)始');
// 宏任務(wù)
setTimeout(() => {
console.log('6. 宏任務(wù) - setTimeout');
Promise.resolve().then(() => {
console.log('7. 微任務(wù) - 在宏任務(wù)中');
});
}, 0);
// 微任務(wù)
Promise.resolve().then(() => {
console.log('4. 微任務(wù) - Promise 1');
});
Promise.resolve().then(() => {
console.log('5. 微任務(wù) - Promise 2');
});
console.log('2. 同步任務(wù)繼續(xù)');
setTimeout(() => {
console.log('8. 另一個(gè)宏任務(wù)');
}, 0);
console.log('3. 同步任務(wù)結(jié)束');
// 執(zhí)行過(guò)程分析:
4.2 事件循環(huán)算法
1. 執(zhí)行同步代碼(調(diào)用棧) 2. 調(diào)用棧清空后,檢查微任務(wù)隊(duì)列 3. 執(zhí)行所有微任務(wù)(直到微任務(wù)隊(duì)列清空) 4. 必要時(shí)進(jìn)行UI渲染 5. 從宏任務(wù)隊(duì)列取一個(gè)任務(wù)執(zhí)行 6. 回到步驟2,循環(huán)...
5. 實(shí)際代碼演示
5.1 復(fù)雜示例分析
console.log('start'); // 1. 同步
setTimeout(function() {
console.log('timeout1'); // 5. 宏任務(wù)
Promise.resolve().then(function() {
console.log('promise1'); // 6. 微任務(wù)
});
}, 0);
Promise.resolve().then(function() {
console.log('promise2'); // 3. 微任務(wù)
setTimeout(function() {
console.log('timeout2'); // 7. 宏任務(wù)
}, 0);
});
console.log('end'); // 2. 同步
// 輸出順序:
// start
// end
// promise2
// timeout1
// promise1
// timeout2
5.2 嵌套任務(wù)執(zhí)行順序
console.log('1. 開(kāi)始');
setTimeout(() => {
console.log('2. 外層宏任務(wù)');
Promise.resolve().then(() => {
console.log('3. 外層微任務(wù)');
});
setTimeout(() => {
console.log('4. 內(nèi)層宏任務(wù)');
}, 0);
}, 0);
Promise.resolve().then(() => {
console.log('5. 外層微任務(wù)');
setTimeout(() => {
console.log('6. 微任務(wù)中的宏任務(wù)');
}, 0);
});
console.log('7. 結(jié)束');
// 執(zhí)行順序分析:
// 1 → 7 → 5 → 2 → 3 → 6 → 4
6. 瀏覽器 vs Node.js 事件循環(huán)差異
6.1 瀏覽器事件循環(huán)
// 瀏覽器中的階段:
// 1. 執(zhí)行同步代碼
// 2. 執(zhí)行微任務(wù)
// 3. UI渲染(如果需要)
// 4. 執(zhí)行宏任務(wù)
console.log('腳本開(kāi)始');
// 宏任務(wù)
setTimeout(() => console.log('計(jì)時(shí)器'));
// 微任務(wù)
Promise.resolve().then(() => console.log('Promise'));
// 動(dòng)畫(huà)幀回調(diào)(在渲染前執(zhí)行)
requestAnimationFrame(() => console.log('RAF'));
console.log('腳本結(jié)束');
6.2 Node.js 事件循環(huán)
// Node.js 有更復(fù)雜的階段:
// timers → pending callbacks → idle, prepare → poll → check → close callbacks
console.log('開(kāi)始');
setTimeout(() => console.log('timer1'), 0);
setImmediate(() => console.log('immediate'));
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
console.log('結(jié)束');
// Node.js 輸出可能:
// 開(kāi)始 → 結(jié)束 → nextTick → promise → timer1 → immediate
// 或者:開(kāi)始 → 結(jié)束 → nextTick → promise → immediate → timer1
7. 常見(jiàn)面試題分析
7.1 經(jīng)典面試題
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5');
});
console.log('6');
// 輸出順序:1 → 4 → 6 → 5 → 2 → 3
7.2 進(jìn)階面試題
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
// 輸出順序:
// script start → async1 start → async2 → promise1 → script end
// → async1 end → promise2 → setTimeout
8. 性能優(yōu)化實(shí)踐
8.1 避免阻塞事件循環(huán)
// 錯(cuò)誤示范:同步耗時(shí)操作阻塞事件循環(huán)
function processLargeData(data) {
// 這個(gè)函數(shù)執(zhí)行時(shí)間很長(zhǎng),會(huì)阻塞頁(yè)面
for (let i = 0; i < 1000000; i++) {
// 繁重的同步計(jì)算
}
}
// 正確示范:將任務(wù)分解為異步執(zhí)行
async function processLargeDataAsync(data) {
for (let i = 0; i < data.length; i += 1000) {
const chunk = data.slice(i, i + 1000);
// 使用 setTimeout 或 Promise 讓出控制權(quán)
await new Promise(resolve => setTimeout(resolve, 0));
processChunk(chunk);
}
}
8.2 合理使用微任務(wù)和宏任務(wù)
// 緊急任務(wù)使用微任務(wù)
function urgentTask() {
Promise.resolve().then(() => {
// 需要立即執(zhí)行的任務(wù)
updateUI();
});
}
// 非緊急任務(wù)使用宏任務(wù)
function nonUrgentTask() {
setTimeout(() => {
// 可以延遲執(zhí)行的任務(wù)
logAnalytics();
}, 0);
}
9. 調(diào)試技巧
9.1 查看任務(wù)隊(duì)列
// 添加調(diào)試信息
let microtaskCount = 0;
let macrotaskCount = 0;
// 包裝 Promise 來(lái)跟蹤微任務(wù)
const originalThen = Promise.prototype.then;
Promise.prototype.then = function(...args) {
microtaskCount++;
console.log(`微任務(wù)創(chuàng)建,總數(shù): ${microtaskCount}`);
return originalThen.apply(this, args);
};
// 跟蹤宏任務(wù)
const originalSetTimeout = window.setTimeout;
window.setTimeout = function(...args) {
macrotaskCount++;
console.log(`宏任務(wù)創(chuàng)建,總數(shù): ${macrotaskCount}`);
return originalSetTimeout.apply(this, args);
};
總結(jié)
事件循環(huán)要點(diǎn):
- 同步代碼優(yōu)先:先執(zhí)行完所有同步任務(wù)
- 微任務(wù)優(yōu)先:微任務(wù)在宏任務(wù)之前執(zhí)行
- 隊(duì)列清空:每個(gè)宏任務(wù)執(zhí)行后都會(huì)清空微任務(wù)隊(duì)列
- 循環(huán)不斷:事件循環(huán)持續(xù)檢查新任務(wù)
記憶口訣:
“同微宏,微先走,宏之后,微清空”
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
JavaScript數(shù)組排序的六種常見(jiàn)算法總結(jié)
這篇文章主要給大家介紹了關(guān)于JavaScript數(shù)組排序的六種常見(jiàn)算法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用JavaScript數(shù)組具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
靈活使用數(shù)組制作圖片切換js實(shí)現(xiàn)
這篇文章主要介紹了靈活使用數(shù)組制作圖片切換效果,js實(shí)現(xiàn)圖片切換特效,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-07-07
JavaScript對(duì)象與數(shù)組的幾種常見(jiàn)復(fù)制方法
在 JavaScript 開(kāi)發(fā)中,對(duì)象和數(shù)組的復(fù)制是一個(gè)非常常見(jiàn)的操作,無(wú)論是函數(shù)參數(shù)傳遞、狀態(tài)管理,還是避免不必要的副作用,了解和掌握各種復(fù)制方式至關(guān)重要,我們將系統(tǒng)梳理 JavaScript 中的幾種常見(jiàn)復(fù)制方法,并對(duì)其應(yīng)用場(chǎng)景進(jìn)行詳細(xì)說(shuō)明,需要的朋友可以參考下2024-10-10
收集的一些Array及String原型對(duì)象的擴(kuò)展實(shí)現(xiàn)代碼
收集的一些Array及String原型對(duì)象的擴(kuò)展實(shí)現(xiàn)代碼,學(xué)習(xí)js的朋友可以參考下。并可以自定義的對(duì)字符串與array數(shù)據(jù),進(jìn)行擴(kuò)展。2010-12-12
javascript利用apply和arguments復(fù)用方法
這篇文章主要介紹了javascript利用apply和arguments復(fù)用方法,有需要的朋友可以參考一下2013-11-11
基于Proxy的小程序狀態(tài)管理實(shí)現(xiàn)
這篇文章主要介紹了基于Proxy的小程序狀態(tài)管理實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-06-06
JS實(shí)現(xiàn)數(shù)組的增刪改查操作示例
這篇文章主要介紹了JS實(shí)現(xiàn)數(shù)組的增刪改查操作,結(jié)合實(shí)例形式分析了javascript針對(duì)數(shù)組的追加、獲取、刪除、添加、修改等常見(jiàn)操作技巧與相關(guān)注意事項(xiàng),需要的朋友可以參考下2018-08-08
詳解template標(biāo)簽用法(含vue中的用法總結(jié))
這篇文章主要介紹了template標(biāo)簽用法(含vue中的用法總結(jié)),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2021-01-01

