JavaScript 事件循环与微任务/宏任务
事件循环(Event Loop)是 JavaScript 异步执行的调度机制,理解微任务与宏任务的执行顺序是掌握异步的关键。
事件循环模型
JavaScript
// JavaScript 运行时结构
// ┌───────────────────────────┐
// │ Call Stack(调用栈) │
// │ 当前执行的函数 │
// └───────────────────────────┘
// ↓
// ┌───────────────────────────┐
// │ Microtask Queue │ ← 微任务(Promise.then 等)
// │ 执行完栈后立即清空 │
// └───────────────────────────┘
// ↓
// ┌───────────────────────────┐
// │ Macrotask Queue │ ← 宏任务(setTimeout 等)
// │ 每次循环取一个执行 │
// └───────────────────────────┘
// ↓
// ┌───────────────────────────┐
// │ Rendering(渲染) │ ← UI 更新(可选)
// └───────────────────────────┘
执行顺序规则
JavaScript
// 事件循环执行流程:
// 1. 执行调用栈中的同步代码
// 2. 调用栈清空后,执行所有微任务
// 3. 执行一个宏任务
// 4. 渲染更新(如果有)
// 5. 回到步骤 2
console.log('1. 同步开始');
setTimeout(() => console.log('2. 宏任务 setTimeout'), 0);
Promise.resolve()
.then(() => console.log('3. 微任务 Promise.then 1'))
.then(() => console.log('4. 微任务 Promise.then 2'));
console.log('5. 同步结束');
// 输出顺序:
// 1. 同步开始
// 5. 同步结束
// 3. 微任务 Promise.then 1
// 4. 微任务 Promise.then 2
// 2. 宏任务 setTimeout
宏任务 Macrotask
JavaScript
// 宏任务来源:
// - script(整体代码)
// - setTimeout / setInterval
// - setImmediate(Node.js)
// - I/O 操作
// - UI 渲染
// - requestAnimationFrame(浏览器,渲染前)
// setTimeout 延迟最小 4ms
setTimeout(() => console.log('timeout'), 0); // 实际至少 4ms
// setImmediate(Node.js)
setImmediate(() => console.log('immediate'));
// requestAnimationFrame(渲染帧前执行)
requestAnimationFrame(() => console.log('animation frame'));
微任务 Microtask
JavaScript
// 微任务来源:
// - Promise.then / catch / finally
// - queueMicrotask()
// - MutationObserver(DOM 变化监听)
// - process.nextTick(Node.js,优先级最高)
// Promise 创建微任务
Promise.resolve().then(() => console.log('微任务'));
// queueMicrotask 手动添加微任务
queueMicrotask(() => console.log('手动微任务'));
// MutationObserver
const observer = new MutationObserver(() => {
console.log('DOM 变化微任务');
});
observer.observe(document.body, { attributes: true });
document.body.setAttribute('data-test', '1'); // 触发微任务
微任务优先级高于宏任务
JavaScript
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('script end');
// 输出:
// script start
// script end
// promise1
// promise2
// setTimeout
// 解释:
// 1. 同步代码执行:script start → script end
// 2. 调用栈清空,执行所有微任务:promise1 → promise2
// 3. 执行一个宏任务:setTimeout
微任务队列清空机制
JavaScript
// 每次宏任务前,微任务队列必须清空
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
// 解释:
// await 后的代码相当于 Promise.then,加入微任务队列
// 微任务队列全部执行完后才执行 setTimeout
Node.js 事件循环差异
JavaScript
// Node.js 事件循环阶段:
// timers(setTimeout/setInterval)
// pending callbacks
// idle, prepare
// poll(I/O)
// check(setImmediate)
// close callbacks
// process.nextTick 优先级高于 Promise
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Node.js 输出:
// nextTick → promise → timeout/immediate(取决于执行时机)
// setTimeout vs setImmediate
// 在 I/O 回调中:immediate 先于 timeout
const fs = require('fs');
fs.readFile('file', () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// 输出:immediate → timeout(在 poll 阶段后 check 先于 timers)
requestAnimationFrame 时机
JavaScript
// requestAnimationFrame 在渲染前执行
// 不是宏任务也不是微任务,在微任务后、渲染前
console.log('sync');
setTimeout(() => console.log('macro'), 0);
Promise.resolve().then(() => console.log('micro'));
requestAnimationFrame(() => console.log('raf'));
// 输出顺序(浏览器):
// sync → micro → raf → macro
// 微任务 → requestAnimationFrame → 宏任务
// raf 内的微任务在渲染前执行
requestAnimationFrame(() => {
Promise.resolve().then(() => console.log('raf micro'));
console.log('raf sync');
});
// raf sync → raf micro → 渲染
常见面试题解析
JavaScript
// 经典题目
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve()
.then(() => console.log('4'))
.then(() => {
console.log('5');
setTimeout(() => console.log('6'), 0);
});
console.log('7');
// 输出:1 → 7 → 4 → 5 → 2 → 3 → 6
// 解析:
// 第一轮循环:
// 同步:1, 7
// 微任务:4(then1 执行,then2 加入)
// 第二轮循环(微任务继续):
// 微任务:5(then2 执行,setTimeout(6) 加入宏任务)
// 微任务清空
// 第三轮循环:
// 宏任务:setTimeout(2)(then(3) 加入微任务)
// 微任务:3
// 微任务清空
// 第四轮循环:
// 宏任务:setTimeout(6)
注意事项
- 微任务在调用栈清空后立即执行,宏任务等待事件循环
- Promise 回调是微任务,setTimeout 是宏任务
- await 后代码等同于 Promise.then,也是微任务
- Node.js process.nextTick 优先级最高,高于 Promise
JavaScript
// 微任务可能阻塞事件循环
function infiniteMicrotasks() {
Promise.resolve().then(() => {
console.log('微任务');
infiniteMicrotasks(); // 无限添加微任务
});
}
// 宏任务永远无法执行,页面卡死
// 正确做法:定期让出控制权
function batchProcess(items) {
const batch = items.splice(0, 100);
batch.forEach(process);
if (items.length) {
// 使用宏任务让出控制权
setTimeout(() => batchProcess(items), 0);
}
}
要点总结
- 事件循环:同步代码 → 清空微任务 → 一个宏任务 → 循环
- 微任务:Promise.then/catch/finally、queueMicrotask、MutationObserver
- 宏任务:setTimeout/setInterval、setImmediate、I/O、script
- 微任务优先级高于宏任务,每次宏任务前微任务队列必须清空
- await 后代码等同于微任务
- Node.js process.nextTick 优先级最高
- requestAnimationFrame 在微任务后、渲染前执行
- 避免无限微任务阻塞事件循环
📝 发现内容有误?点击此处直接编辑