在JavaScript中,代码的执行顺序涉及事件循环(Event Loop)、调用栈(Call Stack)、任务队列(Task Queue)等核心机制。以下是分层次的解析和典型示例:
同步代码
↓
微任务队列(全部执行)
↓
渲染(如有需要)
↓
宏任务队列(取一个执行)
↓
重复循环
掌握这些规则后,可通过以下步骤分析任何执行顺序问题:
标记所有同步代码
识别微任务(Promise.then, MutationObserver, queueMicrotask)
识别宏任务(setTimeout, setInterval, I/O操作, UI渲染等)
按规则循环处理队列
复杂场景示例
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 的执行分为两个阶段:
1. await async2() 会立即执行 async2() 函数内的同步代码(即 console.log('async2')),
此时不会产生任何任务队列操作。
2. 隐式创建 Promise 并暂停 async1
如果 async2() 返回非 Promise,JS 会用 Promise.resolve() 包装它
await 的本质:将 async1 函数内await 之后的代码
(即 console.log('async1 end'))包装成这个 Promise 的 .then() 回调,也就是放入微任务队列
特殊场景注意
- Promise构造函数是同步的
new Promise(resolve => {
console.log('Promise executor'); // 同步!
resolve();
}).then(() => console.log('then')); // 微任务
- 任务队列的优先级
宏任务队列中:
交互事件(如click) > 网络回调 > 定时器 - Node.js差异
Node中的process.nextTick优先级高于微任务:
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
执行流程:
1. 同步代码:1, 6
2. 微任务队列:4(执行时把5的定时器加入宏任务队列)
3. 宏任务队列:2(执行时把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
关键点:
- 微任务中注册的宏任务:在微任务执行期间注册的宏任务(如 Timeout 3),会被添加到当前宏任务队列的末尾。
它需要等待当前所有已存在的宏任务(如 Timeout 1 和 Timeout 2)执行完毕后才会执行。 - 执行顺序的本质:
同步代码 → 微任务 → 宏任务1 → 宏任务1触发的微任务 → 宏任务2 → 宏任务3...
- 为什么 Timeout 3 最后执行?
因为它是前一个微任务(Promise 2)执行时才注册的,而 Timeout 1 和 Timeout 2 早已在同步阶段就注册了。
类比助记
把任务队列想象成一个快递分拣系统:
同步代码:立即处理的加急快递。
微任务:分拣员手头正在打包的包裹(必须全部打完才能处理新快递)。
宏任务:排队等待的普通快递。
在打包时新收到的快递(如 Timeout 3)会被放到普通队列的队尾,按顺序处理。
再来一个例子【混合事件循环优先级】
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
//手动触发的 click 是同步的,真实点击会优先于 Fetch 和 Timeout。
//微任务 Promise 先于宏任务 Fetch 和 Timeout 执行。
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)优先执行。