在JavaScript中,代碼的執(zhí)行順序涉及事件循環(huán)(Event Loop)、調用棧(Call Stack)、任務隊列(Task Queue)等核心機制。以下是分層次的解析和典型示例:
同步代碼
↓
微任務隊列(全部執(zhí)行)
↓
渲染(如有需要)
↓
宏任務隊列(取一個執(zhí)行)
↓
重復循環(huán)
掌握這些規(guī)則后,可通過以下步驟分析任何執(zhí)行順序問題:
標記所有同步代碼
識別微任務(Promise.then, MutationObserver, queueMicrotask)
識別宏任務(setTimeout, setInterval, I/O操作, UI渲染等)
按規(guī)則循環(huán)處理隊列
復雜場景示例
async function async1() {
console.log('async1 start'); // 同步
await async2(); // 相當于Promise.resolve(async2()).then(...)
console.log('async1 end'); // 微任務
}
async function async2() {
console.log('async2'); // 同步
}
console.log('script start'); // 同步
async1();
new Promise(resolve => {
console.log('Promise'); // 同步
resolve();
}).then(() => {
console.log('Promise then'); // 微任務
});
console.log('script end'); // 同步
/* 輸出:
script start → async1 start → async2 → Promise → script end →
async1 end → Promise then
*/
這里核心概念修正:
await 的執(zhí)行分為兩個階段:
1. await async2() 會立即執(zhí)行 async2() 函數(shù)內的同步代碼(即 console.log('async2')),
此時不會產生任何任務隊列操作。
2. 隱式創(chuàng)建 Promise 并暫停 async1
如果 async2() 返回非 Promise,JS 會用 Promise.resolve() 包裝它
await 的本質:將 async1 函數(shù)內await 之后的代碼
(即 console.log('async1 end'))包裝成這個 Promise 的 .then() 回調,也就是放入微任務隊列
特殊場景注意
- Promise構造函數(shù)是同步的
new Promise(resolve => {
console.log('Promise executor'); // 同步!
resolve();
}).then(() => console.log('then')); // 微任務
- 任務隊列的優(yōu)先級
宏任務隊列中:
交互事件(如click) > 網(wǎng)絡回調 > 定時器 - Node.js差異
Node中的process.nextTick優(yōu)先級高于微任務:
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
// 輸出:nextTick → Promise
再看一個例子
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => console.log('5'), 0);
});
console.log('6');
/* 答案:
1 → 6 → 4 → 2 → 3 → 5
執(zhí)行流程:
1. 同步代碼:1, 6
2. 微任務隊列:4(執(zhí)行時把5的定時器加入宏任務隊列)
3. 宏任務隊列:2(執(zhí)行時把3加入微任務隊列)
4. 微任務隊列:3
5. 宏任務隊列:5
*/
再看一個例子【混合宏任務與嵌套 Promise】
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise 1'));
}, 0);
setTimeout(() => {
console.log('Timeout 2');
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 2');
setTimeout(() => console.log('Timeout 3'), 0);
});
console.log('End');
// Start → End → Promise 2 → Timeout 1 → Promise 1 → Timeout 2 → Timeout 3
關鍵點:
- 微任務中注冊的宏任務:在微任務執(zhí)行期間注冊的宏任務(如 Timeout 3),會被添加到當前宏任務隊列的末尾。
它需要等待當前所有已存在的宏任務(如 Timeout 1 和 Timeout 2)執(zhí)行完畢后才會執(zhí)行。 - 執(zhí)行順序的本質:
同步代碼 → 微任務 → 宏任務1 → 宏任務1觸發(fā)的微任務 → 宏任務2 → 宏任務3...
- 為什么 Timeout 3 最后執(zhí)行?
因為它是前一個微任務(Promise 2)執(zhí)行時才注冊的,而 Timeout 1 和 Timeout 2 早已在同步階段就注冊了。
類比助記
把任務隊列想象成一個快遞分揀系統(tǒng):
同步代碼:立即處理的加急快遞。
微任務:分揀員手頭正在打包的包裹(必須全部打完才能處理新快遞)。
宏任務:排隊等待的普通快遞。
在打包時新收到的快遞(如 Timeout 3)會被放到普通隊列的隊尾,按順序處理。
再來一個例子【混合事件循環(huán)優(yōu)先級】
document.addEventListener('click', () => console.log('Click'));
setTimeout(() => console.log('Timeout'), 0);
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(() => console.log('Fetch'));
Promise.resolve().then(() => console.log('Promise'));
// 模擬用戶點擊(實際點擊時輸出可能不同)
document.dispatchEvent(new Event('click'));
console.log('Sync end');
// Click → Sync end → Promise → Fetch → Timeout
//手動觸發(fā)的 click 是同步的,真實點擊會優(yōu)先于 Fetch 和 Timeout。
//微任務 Promise 先于宏任務 Fetch 和 Timeout 執(zhí)行。
Promise.all 與微任務
console.log('Start');
Promise.all([
new Promise(resolve => {
setTimeout(() => {
console.log('Timeout 1');
resolve();
}, 0);
}),
Promise.resolve().then(() => console.log('Promise 1')),
]).then(() => {
console.log('Promise.all resolved');
});
Promise.resolve()
.then(() => console.log('Promise 2'))
.then(() => console.log('Promise 3'));
console.log('End');
// Start → End → Promise 1 → Promise 2 → Promise 3 → Timeout 1 → Promise.all resolved
//Promise.all 需等待所有子 Promise 完成,包括宏任務 Timeout 1。
//獨立的微任務鏈(Promise 2 → Promise 3)優(yōu)先執(zhí)行。