为什么要区分宏任务和微任务
JavaScript 的异步编程模型看似复杂,实际上是由一个简单的规则驱动的:微任务总是在下一个宏任务之前执行。
这个设计是有原因的。微任务通常涉及需要尽快完成的后续操作,比如 Promise 的回调——Promise 代表一个已经或即将完成的异步操作,它的 .then() 回调需要尽快执行,以响应这个已完成的状态变化。
而宏任务是较大的工作单元,比如用户交互事件、计时器回调、I/O 操作。这些不需要在当前宏任务结束后立即执行,可以在浏览器/Node.js 方便的时候执行。
理解这个区别,不只能帮你做对面试题,更能帮你在实际项目中写出正确的异步代码。
宏任务的完整分类
浏览器环境中的宏任务
| API | 阶段 | 说明 |
|---|---|---|
| setTimeout / setInterval | timers | 计时器回调 |
| setImmediate | check | Node.js 专用 |
| I/O callbacks | pending callbacks | 上一轮 I/O 的回调 |
| requestAnimationFrame | before the next frame | 浏览器渲染前 |
| UI 事件(click, keydown) | 宏任务 | 用户交互 |
| requestIdleCallback | 空档期 | 浏览器空闲时执行 |
Node.js 环境中的宏任务
Node.js 的宏任务阶段更细分:
- timers:
setTimeout、setInterval回调 - pending callbacks:延迟到下一轮 I/O 的回调
- idle, prepare:内部使用
- poll:获取新的 I/O 事件
- check:
setImmediate回调 - close callbacks:关闭回调
requestAnimationFrame 的特殊性
requestAnimationFrame 是一个特殊的宏任务。它在浏览器中每一帧的渲染前被调用,大约是每秒 60 次(16.67ms 一帧)。
// 动画的正确实现方式
function animate() {
// 更新动画状态
updateAnimation();
// 浏览器会在合适时机渲染
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
相比之下,setTimeout(..., 16) 不能保证在每一帧执行,可能会跳帧或卡顿。
微任务的完整分类
微任务家族
| API | 环境 | 说明 |
|---|---|---|
| Promise.then/catch/finally | 通用 | Promise 回调 |
| queueMicrotask() | 通用 | 手动入队微任务 |
| MutationObserver | 浏览器 | DOM 变动观察 |
| process.nextTick | Node.js | 最高优先级 |
queueMicrotask 的使用
queueMicrotask() 允许你手动将一个函数加入微任务队列:
queueMicrotask(() => {
console.log('microtask');
});
console.log('sync');
// 输出: sync, microtask
这在你需要确保某个操作在当前任务完成后、渲染发生前执行时很有用。
Promise 的微任务特性
Promise 的 .then()、.catch()、.finally() 都是微任务。但要注意:Promise 的 executor 函数是同步执行的。
const promise = new Promise((resolve, reject) => {
console.log('executor'); // 同步执行
resolve('value');
});
promise.then(value => {
console.log('then'); // 微任务
});
console.log('sync'); // 同步执行
// 输出: executor, sync, then
嵌套的微任务
微任务可以嵌套,但每个微任务都会在下一个宏任务开始前被完全执行:
Promise.resolve()
.then(() => {
console.log('microtask 1');
Promise.resolve().then(() => {
console.log('nested microtask');
});
})
.then(() => {
console.log('microtask 2');
});
// 输出: microtask 1, nested microtask, microtask 2
注意 microtask 2 在嵌套微任务之后输出,因为第二个 .then() 本身也是一个微任务,它依赖于第一个 .then() 执行完毕才加入队列。
async/await 与微任务
async 函数返回一个 Promise,await 暂停当前函数的执行,将后续代码作为微任务加入队列:
async function foo() {
console.log('1');
await bar();
console.log('2');
}
function bar() {
return Promise.resolve();
}
console.log('3');
foo();
console.log('4');
// 输出: 3, 1, 4, 2
执行过程:
console.log('3')同步输出- 调用
foo(),输出1 await Promise.resolve()暂停foo,将console.log('2')加入微任务队列console.log('4')同步输出- 微任务执行,输出
2
常见面试题解析
题目一
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
queueMicrotask(() => console.log('queueMicrotask'));
console.log('sync');
答案:sync, Promise, queueMicrotask, setTimeout
解析:
sync是同步任务,最先输出Promise.then和queueMicrotask都是微任务,在同步任务后执行queueMicrotask先于Promise.then因为它是同步代码后面第一个入队的微任务setTimeout是宏任务,在所有微任务执行完毕后执行
题目二
async function foo() {
console.log('a');
await Promise.resolve();
console.log('b');
}
console.log('c');
setTimeout(() => console.log('d'), 0);
foo().then(() => console.log('e'));
Promise.resolve().then(() => console.log('f'));
console.log('g');
答案:c, a, g, b, f, e, d
解析:
c同步输出foo()调用,输出a,await将b加入微任务队列g同步输出- 微任务检查:
b入队,然后foo().then()的回调入队,然后Promise.resolve().then()的回调入队 - 微任务执行:
b输出,f输出,e输出 - 宏任务执行:
d输出
实际开发中的应用
防抖与节流中的微任务
// 防抖:最后一次操作后才执行
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 节流:固定间隔执行
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
两者的区别在于:防抖用 setTimeout,是宏任务;节流如果用 requestAnimationFrame,会在渲染前执行。
React 中的微任务应用
React 的 flushSync 用于强制同步执行:
import { flushSync } from 'react-dom';
flushSync(() => {
setState({ count: 1 });
});
console.log('同步更新');
React 18 的自动批处理(automatic batching)也涉及微任务——状态更新会被合并,在微任务结束时一起应用。
这一章想说的
宏任务和微任务的区别是 JavaScript 异步编程的核心。
宏任务是宿主环境发起的大任务,每次事件循环迭代执行一个;微任务是当前宏任务结束后立即执行的任务,优先级更高。
常见的宏任务:setTimeout、setInterval、I/O、requestAnimationFrame。
常见的微任务:Promise.then、queueMicrotask、MutationObserver。
记住一个核心规则:微任务总是在下一个宏任务之前全部执行。理解这个规则,你就能分析任何复杂的异步执行顺序问题。
延展阅读
- MDN: queueMicrotask() — 微任务入队 API 的官方文档
- JavaScript Visualized: Event Loop — Lydia Hallie 的可视化系列
- Node.js Event Loop docs — Node.js 官方事件循环文档
实践练习
练习:分析并验证以下代码的执行顺序
async function test() {
console.log('1');
await new Promise(resolve => {
console.log('2');
resolve();
}).then(() => console.log('3'));
console.log('4');
await new Promise(resolve => {
console.log('5');
resolve();
}).then(() => console.log('6'));
}
setTimeout(() => {
console.log('7');
Promise.resolve().then(() => console.log('8'));
}, 0);
test().then(() => console.log('9'));
console.log('10');
先自己分析输出顺序,然后运行代码验证。重点理解 await 的执行流程——await 后面的代码是在微任务里执行的。