我們首先需要理解JavaScript的事件循環(huán)(Event Loop)機(jī)制,因?yàn)樗荍avaScript異步編程的核心。事件循環(huán)允許JavaScript在執(zhí)行非阻塞I/O操作時(shí)保持高效,盡管它是單線程的。
事件循環(huán)的基本概念:
JavaScript運(yùn)行時(shí)包含一個(gè)消息隊(duì)列(或任務(wù)隊(duì)列),用于存儲(chǔ)待處理的消息(任務(wù))。每個(gè)消息都關(guān)聯(lián)著一個(gè)回調(diào)函數(shù)。事件循環(huán)會(huì)不斷地從消息隊(duì)列中取出消息并執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。
事件循環(huán)中有兩種主要的任務(wù)隊(duì)列:
宏任務(wù)(Macrotasks):包括整體script代碼、setTimeout、setInterval、I/O、UI渲染等。
微任務(wù)(Microtasks):包括Promise回調(diào)、MutationObserver、process.nextTick(Node.js)等。
事件循環(huán)的執(zhí)行順序:
執(zhí)行一個(gè)宏任務(wù)(從宏任務(wù)隊(duì)列中取出一個(gè)任務(wù)執(zhí)行)。
執(zhí)行過(guò)程中遇到微任務(wù),將其添加到微任務(wù)隊(duì)列。
宏任務(wù)執(zhí)行完畢,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)。
微任務(wù)執(zhí)行完畢,開始下一個(gè)宏任務(wù)(如果有的話)。
如此循環(huán),直到所有任務(wù)完成。
常見問(wèn)題:
1. 請(qǐng)解釋事件循環(huán)(Event Loop)是什么?
事件循環(huán)是JavaScript處理異步操作的一種機(jī)制。它通過(guò)一個(gè)循環(huán)不斷地檢查任務(wù)隊(duì)列中是否有任務(wù)需要執(zhí)行,如果有則取出執(zhí)行。它分為宏任務(wù)和微任務(wù),每次執(zhí)行一個(gè)宏任務(wù)后,會(huì)清空整個(gè)微任務(wù)隊(duì)列。
2. 宏任務(wù)(Macrotask)和微任務(wù)(Microtask)有什么區(qū)別?
宏任務(wù):由宿主環(huán)境(瀏覽器、Node.js)發(fā)起的任務(wù),如setTimeout、setInterval、I/O、UI渲染、事件回調(diào)等。
微任務(wù):由JavaScript引擎發(fā)起的任務(wù),如Promise回調(diào)、MutationObserver、process.nextTick(Node.js)等。
執(zhí)行順序:每次事件循環(huán)中,先執(zhí)行一個(gè)宏任務(wù),然后執(zhí)行所有微任務(wù),再執(zhí)行下一個(gè)宏任務(wù),如此循環(huán)。
3. 以下代碼的輸出順序是什么?
console.log('1');
setTimeout(function() {
console.log('2');
}, 0);
Promise.resolve().then(function() {
console.log('3');
});
console.log('4');
輸出順序:1, 4, 3, 2
解釋:
首先執(zhí)行同步代碼:輸出1和4。
然后檢查微任務(wù)隊(duì)列,有Promise回調(diào),輸出3。
最后執(zhí)行宏任務(wù)隊(duì)列中的setTimeout回調(diào),輸出2。
4. 如果嵌套宏任務(wù)和微任務(wù),執(zhí)行順序如何?
console.log('start');
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
setTimeout(() => {
console.log('timeout2');
Promise.resolve().then(() => {
console.log('promise2');
});
}, 0);
Promise.resolve().then(() => {
console.log('promise3');
});
console.log('end');
輸出順序:start, end, promise3, timeout1, promise1, timeout2, promise2
解釋:
同步代碼:輸出start和end。
微任務(wù)隊(duì)列:執(zhí)行Promise回調(diào),輸出promise3。
宏任務(wù)隊(duì)列:第一個(gè)setTimeout回調(diào),輸出timeout1,然后其內(nèi)部的Promise回調(diào)加入微任務(wù)隊(duì)列,執(zhí)行微任務(wù)(輸出promise1)。
接著執(zhí)行第二個(gè)setTimeout回調(diào),輸出timeout2,然后其內(nèi)部的Promise回調(diào)加入微任務(wù)隊(duì)列,執(zhí)行微任務(wù)(輸出promise2)。
5. setTimeout(fn, 0) 真的在0毫秒后執(zhí)行嗎?
不一定。它表示在至少0毫秒后執(zhí)行,即盡快執(zhí)行,但實(shí)際執(zhí)行時(shí)間取決于當(dāng)前執(zhí)行棧是否為空以及消息隊(duì)列中是否有其他任務(wù)在等待。因?yàn)镴avaScript是單線程的,如果當(dāng)前有任務(wù)在執(zhí)行,那么setTimeout的回調(diào)必須等待。
6. Node.js中的事件循環(huán)和瀏覽器中的事件循環(huán)有什么區(qū)別?
在Node.js中,事件循環(huán)分為多個(gè)階段(如timers、pending callbacks、idle, prepare、poll、check、close callbacks),每個(gè)階段執(zhí)行特定的任務(wù)。而瀏覽器中的事件循環(huán)沒有這樣的階段劃分,但同樣有宏任務(wù)和微任務(wù)的概念。
在Node.js中,process.nextTick回調(diào)比微任務(wù)還要優(yōu)先執(zhí)行,即在每個(gè)階段切換時(shí)都會(huì)執(zhí)行process.nextTick隊(duì)列。
7. 什么是任務(wù)隊(duì)列(Task Queue)和作業(yè)隊(duì)列(Job Queue)?
任務(wù)隊(duì)列通常指宏任務(wù)隊(duì)列,而作業(yè)隊(duì)列通常指微任務(wù)隊(duì)列。在ECMAScript規(guī)范中,微任務(wù)隊(duì)列被稱為Job Queue。
8. 如何理解異步編程中的“饑餓”問(wèn)題?
如果微任務(wù)中不斷地產(chǎn)生新的微任務(wù),那么宏任務(wù)將永遠(yuǎn)得不到執(zhí)行,導(dǎo)致“饑餓”。因此,在編寫代碼時(shí)要注意避免微任務(wù)的無(wú)限循環(huán)。
總結(jié):
事件循環(huán)是JavaScript異步的基礎(chǔ),理解宏任務(wù)和微任務(wù)的執(zhí)行順序是解決異步代碼執(zhí)行順序問(wèn)題的關(guān)鍵。通常會(huì)通過(guò)代碼輸出題來(lái)考察對(duì)事件循環(huán)的理解。同時(shí),也要注意Node.js和瀏覽器環(huán)境在事件循環(huán)上的差異
JavaScript 事件循環(huán)(Event Loop)深度解析與高頻問(wèn)題
一、事件循環(huán)核心概念
JavaScript 是單線程語(yǔ)言,事件循環(huán)是其實(shí)現(xiàn)異步編程的核心機(jī)制:
┌───────────────────────┐
┌─>│ 調(diào)用棧 │<───執(zhí)行上下文
│ └──────────┬────────────┘
│ │
│ ┌──────────┴────────────┐
│ │ 微任務(wù)隊(duì)列 │<───Promise/MutationObserver/process.nextTick
│ └──────────┬────────────┘
│ │
│ ┌──────────┴────────────┐
│ │ 宏任務(wù)隊(duì)列 │<───setTimeout/setInterval/I/O/UI渲染/事件回調(diào)
│ └──────────┬────────────┘
│ │
└─────────────┘
二、執(zhí)行順序規(guī)則
- 同步代碼:優(yōu)先執(zhí)行調(diào)用棧中的同步任務(wù)
- 微任務(wù):當(dāng)調(diào)用棧清空后,執(zhí)行所有微任務(wù)
- 宏任務(wù):每次從宏任務(wù)隊(duì)列取一個(gè)任務(wù)執(zhí)行
- 渲染:在宏任務(wù)之間執(zhí)行 UI 渲染
三、高頻題及解析
1. 基礎(chǔ)執(zhí)行順序題
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => console.log('4'));
console.log('5');
輸出順序:1 → 5 → 4 → 2 → 3
解析:
- 同步代碼:1, 5
- 微任務(wù):4
- 宏任務(wù)(setTimeout):2
- 宏任務(wù)中的微任務(wù):3
2. 混合微任務(wù)與宏任務(wù)
setTimeout(() => console.log('A'), 0);
Promise.resolve().then(() => {
console.log('B');
setTimeout(() => console.log('C'), 0);
});
Promise.resolve().then(() => console.log('D'));
console.log('E');
輸出順序:E → B → D → A → C
解析:
- 同步代碼:E
- 微任務(wù)隊(duì)列:
- 第一個(gè) Promise:B(添加宏任務(wù)C)
- 第二個(gè) Promise:D
- 宏任務(wù)隊(duì)列:
- A(先進(jìn)入隊(duì)列)
- C(后進(jìn)入隊(duì)列)
3. async/await 執(zhí)行順序
async function async1() {
console.log('A');
await async2();
console.log('B');
}
async function async2() {
console.log('C');
}
console.log('D');
setTimeout(() => console.log('E'), 0);
async1();
new Promise(resolve => {
console.log('F');
resolve();
}).then(() => console.log('G'));
console.log('H');
輸出順序:D → A → C → F → H → B → G → E
解析:
-
await后面的代碼相當(dāng)于放在Promise.then中 - 等價(jià)轉(zhuǎn)換:
// async1 轉(zhuǎn)換為: function async1() { console.log('A'); new Promise(resolve => { async2(); resolve(); }).then(() => console.log('B')); }
4. Node.js 與瀏覽器差異
setTimeout(() => console.log('1'), 0);
setImmediate(() => console.log('2'));
process.nextTick(() => console.log('3'));
Promise.resolve().then(() => console.log('4'));
console.log('5');
瀏覽器輸出:5 → 4 → 1 → 2 → 3(nextTick 非標(biāo)準(zhǔn))
Node.js 輸出:5 → 3 → 4 → 1 或 5 → 3 → 4 → 2 → 1
差異點(diǎn):
-
process.nextTick優(yōu)先級(jí)最高(Node獨(dú)有) -
setImmediate與setTimeout(0)順序不確定 - Node 有多個(gè)階段:timers → pending → idle → poll → check → close
5. 復(fù)雜嵌套場(chǎng)景
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise 1'));
}, 0);
setTimeout(() => {
console.log('Timeout 2');
Promise.resolve().then(() => {
console.log('Promise 2');
setTimeout(() => console.log('Timeout 3'), 0);
});
}, 0);
Promise.resolve().then(() => console.log('Promise 3'));
console.log('End');
輸出順序:
Start → End → Promise 3 → Timeout 1 → Promise 1 → Timeout 2 → Promise 2 → Timeout 3
四、事件循環(huán)核心知識(shí)點(diǎn)
-
任務(wù)隊(duì)列類型:
- 宏任務(wù):script、setTimeout、setInterval、I/O、UI渲染
- 微任務(wù):Promise.then、MutationObserver、process.nextTick(Node)
-
關(guān)鍵執(zhí)行規(guī)則:
- 每執(zhí)行一個(gè)宏任務(wù)后,清空整個(gè)微任務(wù)隊(duì)列
- UI 渲染在宏任務(wù)之間執(zhí)行
-
requestAnimationFrame在渲染前執(zhí)行
-
Node.js 特殊機(jī)制:
┌───────────────────────┐ │ timers │<── setTimeout/setInterval ├───────────────────────┤ │ pending callbacks │<── I/O回調(diào) ├───────────────────────┤ │ idle, prepare │<── 內(nèi)部使用 ├───────────────────────┤ │ poll │<── 檢索新I/O事件 ├───────────────────────┤ │ check │<── setImmediate ├───────────────────────┤ │ close callbacks │<── 關(guān)閉事件回調(diào) └───────────────────────┘
五、問(wèn)題必備技巧
-
分析代碼時(shí)先標(biāo)記:
- [S] 同步代碼
- [M] 宏任務(wù)
- [m] 微任務(wù)
-
解題步驟:
- 執(zhí)行所有同步代碼
- 執(zhí)行所有微任務(wù)
- 執(zhí)行一個(gè)宏任務(wù)
- 重復(fù)步驟2-3
-
常見陷阱:
// 阻塞事件循環(huán) while (true) {} // 會(huì)阻塞所有任務(wù) // 微任務(wù)遞歸 function recursiveMicrotask() { Promise.resolve().then(recursiveMicrotask); }
掌握事件循環(huán)機(jī)制是JavaScript高級(jí)開發(fā)的必備技能,建議通過(guò)Chrome DevTools的Performance面板實(shí)時(shí)觀察調(diào)用棧執(zhí)行過(guò)程加深理解。