console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
// 输出:1 4 3 2setTimeout(fn, 0) 不会”立即执行”。JavaScript 的单线程背后有一套精密的调度机制——Event Loop。这道题同时考了同步/异步、宏任务/微任务、事件循环三层知识。
浏览器运行时架构
┌──────────────────────────────────┐
│ JavaScript 引擎 │
│ ┌──────────┐ │
│ │Call Stack│ ← 同步代码执行 │
│ │ [main] │ 一次只做一件事 │
│ └──────────┘ │
└──────────────────────────────────┘
↕ 调用 ↕ 调度
┌────────────┐ ┌──────────────┐
│ Web APIs │ │ Event Loop │
│ setTimeout │ │ (持续轮询) │
│ DOM Events │ └──────────────┘
│ fetch/XHR │ ↕
└────────────┘ ┌────────────────┐
↓ 放入 │MicroTask Queue │ ← Promise.then
┌─────────┐ │(微任务,优先) │ await (之后)
│MacroTask│ └────────────────┘
│ Queue │
│(宏任务) │
└─────────┘
一步步推演
console.log('Hi');
setTimeout(function cb() {
console.log('cb');
}, 5000);
console.log('Bye');
// 输出:Hi → Bye → (5 秒后) cb| 步骤 | Call Stack | 发生了什么 |
|---|---|---|
| 1 | console.log('Hi') | 执行,打印 Hi,弹出 |
| 2 | setTimeout(cb, 5000) | 调用 Web APIs,启动 5 秒定时器。计时由浏览器负责,不占 JS 线程 |
| 3 | console.log('Bye') | 执行,打印 Bye,弹出 |
| 4 | 空闲 | Call Stack 清空,Event Loop 开始轮询 |
| 5 | 空闲 | 5 秒后,Web APIs 将 cb 放入 MacroTask Queue |
| 6 | cb() | Event Loop 取出 cb 推入 Call Stack 执行,打印 cb |
DOM 事件也走同一套机制——click 事件发生时,回调放入宏任务队列,等 Call Stack 空闲后才调度。所以如果主线程被占满(比如长循环),点击事件的响应会延迟甚至”卡住”。
宏任务 vs 微任务
| 类型 | 定义方 | 包含 |
|---|---|---|
| 宏任务 (MacroTask) | 浏览器(W3C/WHATWG) | setTimeout、setInterval、I/O、UI rendering、postMessage、setImmediate(Node) |
| 微任务 (MicroTask) | ECMAScript 规范 | Promise.then/catch/finally、await(之后)、queueMicrotask()、MutationObserver |
一个完整的 Event Loop 周期
1. 从 MacroTask Queue 取出一个宏任务执行(首次执行是整体脚本——也是一个宏任务)
2. 执行过程中产生的微任务进入 MicroTask Queue
3. 宏任务执行完毕,Call Stack 清空
4. 清空 MicroTask Queue(包括微任务中新增的微任务——这是深度的清空)
5. 判断是否需要渲染(不一定每个 cycle 都渲染,浏览器有自己的节流策略)
6. 如需要渲染,执行渲染(Style → Layout → Paint → Composite)
7. requestAnimationFrame 回调执行
8. 回到步骤 1,取出下一个宏任务
核心区别:微任务队列是一次清空(直到队列为空),宏任务队列是一次取一个。
验证
console.log(100);
setTimeout(() => console.log(200)); // 宏任务
Promise.resolve().then(() => console.log(300)); // 微任务
console.log(400);
// 输出:100 400 300 200执行过程:
- 整体脚本作为宏任务执行,打印
100 setTimeout→ 回调放入宏任务队列Promise.then→ 回调放入微任务队列- 打印
400,当前宏任务结束 - 清空微任务队列 → 打印
300 - 取出下一个宏任务 → 打印
200
微任务中创建微任务
Promise.resolve().then(() => {
console.log(1);
Promise.resolve().then(() => console.log(2));
});
Promise.resolve().then(() => console.log(3));
// 输出:1 3 2
// 解释:第一轮清空微任务时打印 1 和 3,但 1 中又创建了新的微任务 2
// 微任务清空是深度的——2 在下一轮微任务检查时被立即执行为什么微任务比宏任务早
Promise 是 ES 规范定义的,回调直接进入 MicroTask Queue——不经过 Web APIs 的异步延迟。而 setTimeout 的路径是:Web APIs(启动计时)→ MacroTask Queue → Event Loop 调度。每次宏任务结束后,引擎会立刻清空当轮积累的所有微任务。
DOM 渲染与 Event Loop
微任务在 DOM 渲染之前执行,宏任务通常在 DOM 渲染之后执行:
$('#container').append($('<p>一段文字</p>'));
Promise.resolve().then(() => {
console.log($('#container').children().length); // 3 —— DOM 结构已更新
alert('Promise then'); // 页面还没绘制新元素
});
setTimeout(() => {
alert('setTimeout'); // 页面已经可以看到新元素了
});微任务在渲染前执行这一特性,使得 Vue 的
nextTick(默认用微任务)能够在 DOM 更新后、浏览器绘制前拿到最新的 DOM 状态。React 的批量更新也是类似思路。
requestAnimationFrame 的位置
requestAnimationFrame(rAF)在渲染之前调用,但也在微任务之后:
宏任务结束 → 清空微任务 → requestAnimationFrame → 渲染 → 下一个宏任务
setTimeout(() => console.log('宏任务'));
requestAnimationFrame(() => console.log('rAF'));
Promise.resolve().then(() => console.log('微任务'));
// 输出:微任务 → rAF → 宏任务综合执行顺序
async function async1() {
console.log('async1 start'); // 2. 同步
await async2(); // async2 函数体同步执行
console.log('async1 end'); // 6. 微任务
}
async function async2() {
console.log('async2'); // 3. 同步
}
console.log('script start'); // 1. 同步
setTimeout(() => {
console.log('setTimeout'); // 8. 宏任务
}, 0);
async1();
new Promise((resolve) => {
console.log('promise1'); // 4. 同步(Promise 构造函数立即执行)
resolve();
}).then(() => {
console.log('promise2'); // 7. 微任务
});
console.log('script end'); // 5. 同步
// 输出顺序:script start → async1 start → async2 → promise1 → script end
// → async1 end → promise2 → setTimeout几个关键点:
await async2()——async2的函数体同步执行,只有await后面的代码才进入微任务new Promise(fn)——fn同步执行,只有.then()的回调才进入微任务await后的代码等价于Promise.resolve().then(...)
Node.js 的 Event Loop
Node.js 的 Event Loop 与浏览器不同,分为六个阶段:
timers → pending callbacks → idle, prepare → poll → check → close callbacks
process.nextTick 和 Promise 是 Node 中的微任务,nextTick 的优先级高于 Promise。setImmediate 在 check 阶段执行,setTimeout(fn, 0) 在 timers 阶段执行。