你以为JavaScript是单线程的,但它却用事件循环实现了"伪异步"。理解宏任务和微任务,是掌握现代前端异步编程的关键。
引言:从一道经典面试题说起
javascript
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
// 输出顺序是什么?
如果你的答案是"1, 4, 3, 2",那么恭喜你已经理解了事件循环的基本概念。但事件循环远不止于此...
第一部分:JavaScript运行环境的真相
1.1 为什么JavaScript是单线程的?
JavaScript最初被设计为浏览器脚本语言,主要用于处理DOM操作。多线程同时操作DOM会带来复杂的同步问题。因此,JavaScript采用了单线程+事件循环的模型。
javascript
// 浏览器中的JavaScript执行环境
┌───────────────────────────┐
│ JavaScript │ ← 单线程执行
│ Engine (V8/SpiderMonkey)│
└───────────────────────────┘
↑ ↓
┌───────────────────────────┐
│ Web APIs (浏览器提供) │ ← 异步API:setTimeout、DOM事件、Ajax等
└───────────────────────────┘
↑ ↓
┌───────────────────────────┐
│ Task Queue (任务队列) │ ← 待执行的回调函数
└───────────────────────────┘
1.2 事件循环的基本原理
javascript
// 事件循环的简化模型
while (eventLoop.waitForTask()) {
// 1. 从任务队列中取出一个任务
const task = eventLoop.getNextTask();
// 2. 执行任务
try {
task();
} catch (error) {
console.error('任务执行出错:', error);
}
// 3. 执行所有微任务
eventLoop.processMicrotasks();
// 4. 渲染(如果需要)
if (shouldRender()) {
eventLoop.render();
}
}
第二部分:宏任务 vs 微任务
2.1 什么是宏任务?
宏任务(MacroTask)代表一个独立的、完整的工作单元。每个宏任务执行完后,浏览器可能会进行渲染。
常见的宏任务:
-
script(整体代码)
-
setTimeout / setInterval
-
setImmediate(Node.js)
-
I/O操作
-
UI渲染(浏览器)
-
事件回调(click、load等)
-
MessageChannel
javascript
// 宏任务示例
console.log('脚本开始'); // 这是第一个宏任务
setTimeout(() => {
console.log('setTimeout回调'); // 新的宏任务
}, 0);
button.addEventListener('click', () => {
console.log('按钮点击'); // 事件回调是宏任务
});
// 当前宏任务结束
2.2 什么是微任务?
微任务(MicroTask)是在当前宏任务结束后、下一个宏任务开始前立即执行的任务。微任务队列会在每个宏任务执行完毕后清空。
常见的微任务:
-
Promise.then / .catch / .finally
-
async/await(本质是Promise)
-
MutationObserver(浏览器)
-
process.nextTick(Node.js,优先级最高)
-
queueMicrotask API
javascript
// 微任务示例
console.log('开始');
Promise.resolve().then(() => {
console.log('Promise 1'); // 微任务
}).then(() => {
console.log('Promise 2'); // 微任务
});
queueMicrotask(() => {
console.log('queueMicrotask'); // 微任务
});
console.log('结束');
// 输出:开始 → 结束 → Promise 1 → Promise 2 → queueMicrotask
2.3 完整的执行顺序
javascript
// 完整的事件循环顺序示例
console.log('1 - 同步代码(宏任务开始)');
setTimeout(() => {
console.log('2 - setTimeout(宏任务)');
Promise.resolve().then(() => {
console.log('3 - 内层Promise(微任务)');
});
}, 0);
Promise.resolve().then(() => {
console.log('4 - 外层Promise(微任务)');
setTimeout(() => {
console.log('5 - 内层setTimeout(宏任务)');
}, 0);
});
console.log('6 - 同步代码(宏任务结束)');
// 执行顺序分析:
// 1. 执行当前宏任务(整体代码):输出 1, 6
// 2. 执行微任务队列:输出 4
// 3. 执行下一个宏任务(第一个setTimeout):输出 2
// 4. 执行该宏任务产生的微任务:输出 3
// 5. 执行下一个宏任务(第二个setTimeout):输出 5
第三部分:浏览器与Node.js的事件循环差异
3.1 浏览器的事件循环模型
javascript
// 浏览器事件循环阶段
┌───────────────────────┐
│ 宏任务队列 │
│ 1. 执行一个宏任务 │
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ 微任务队列 │
│ 2. 执行所有微任务 │
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ requestAnimation │
│ 3. 执行RAF回调 │
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ 渲染阶段 │
│ 4. 样式计算、布局、绘制 │
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ requestIdleCallback │
│ 5. 执行RIC回调(空闲时)│
└───────────────────────┘
3.2 Node.js的事件循环模型
javascript
// Node.js事件循环阶段(更复杂)
┌───────────────────────────┐
│ timers阶段 │ ← 执行setTimeout/setInterval回调
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ pending callbacks阶段 │ ← 执行上一轮未执行的I/O回调
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ idle, prepare阶段 │ ← 内部使用
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ poll阶段 │ ← 检索新的I/O事件,执行相关回调
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ check阶段 │ ← 执行setImmediate回调
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ close callbacks阶段 │ ← 执行close事件的回调
└───────────────────────────┘
3.3 关键差异对比
javascript
// 差异1:process.nextTick vs Promise
Promise.resolve().then(() => {
console.log('Promise');
});
process.nextTick(() => {
console.log('nextTick'); // 先执行
});
// 差异2:setTimeout vs setImmediate
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate'); // 顺序不确定,取决于当前执行环境
});
// 差异3:浏览器 vs Node.js的微任务执行时机
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
setTimeout(() => {
console.log('timeout2');
Promise.resolve().then(() => {
console.log('promise2');
});
}, 0);
// 浏览器输出:timeout1 → promise1 → timeout2 → promise2
// Node.js可能输出:timeout1 → timeout2 → promise1 → promise2
第四部分:实战应用场景
4.1 性能优化:避免阻塞渲染
javascript
// 不好:大量同步任务阻塞渲染
function processLargeArray(array) {
const results = [];
for (let i = 0; i < array.length; i++) {
// 昂贵的计算
results.push(expensiveCalculation(array[i]));
}
return results;
}
// 好:将任务分解为多个宏任务
function processLargeArrayAsync(array, chunkSize = 100) {
return new Promise((resolve) => {
const results = [];
let index = 0;
function processChunk() {
const end = Math.min(index + chunkSize, array.length);
for (; index < end; index++) {
results.push(expensiveCalculation(array[index]));
}
if (index < array.length) {
// 使用setTimeout让出控制权,允许渲染
setTimeout(processChunk, 0);
} else {
resolve(results);
}
}
processChunk();
});
}
// 更好:使用微任务避免不必要的渲染
function processLargeArrayMicrotask(array, chunkSize = 100) {
return new Promise((resolve) => {
const results = [];
let index = 0;
function processChunk() {
const end = Math.min(index + chunkSize, array.length);
for (; index < end; index++) {
results.push(expensiveCalculation(array[index]));
}
if (index < array.length) {
// 使用queueMicrotask,在当前任务结束后立即执行
queueMicrotask(processChunk);
} else {
resolve(results);
}
}
processChunk();
});
}
4.2 实现优先级调度
javascript
class TaskScheduler {
constructor() {
this.microTasks = [];
this.macroTasks = [];
this.isProcessing = false;
}
// 添加高优先级任务(微任务)
addMicrotask(task) {
this.microTasks.push(task);
this.scheduleRun();
}
// 添加普通任务(宏任务)
addMacrotask(task) {
this.macroTasks.push(task);
this.scheduleRun();
}
scheduleRun() {
if (this.isProcessing) return;
this.isProcessing = true;
// 使用微任务来启动处理
queueMicrotask(() => {
this.processTasks();
});
}
processTasks() {
// 先处理所有微任务
while (this.microTasks.length > 0) {
const task = this.microTasks.shift();
try {
task();
} catch (error) {
console.error('微任务执行失败:', error);
}
}
// 然后处理一个宏任务
if (this.macroTasks.length > 0) {
const task = this.macroTasks.shift();
try {
task();
} catch (error) {
console.error('宏任务执行失败:', error);
}
}
// 如果还有任务,继续调度
if (this.microTasks.length > 0 || this.macroTasks.length > 0) {
this.scheduleRun();
} else {
this.isProcessing = false;
}
}
}
// 使用示例
const scheduler = new TaskScheduler();
scheduler.addMacrotask(() => console.log('宏任务 1'));
scheduler.addMicrotask(() => console.log('微任务 1'));
scheduler.addMacrotask(() => console.log('宏任务 2'));
scheduler.addMicrotask(() => console.log('微任务 2'));
// 输出:微任务 1 → 微任务 2 → 宏任务 1 → 宏任务 2
4.3 实现防抖与节流的升级版
javascript
// 使用微任务优化的防抖
function debounceMicrotask(fn, delay) {
let timerId = null;
let microtaskQueued = false;
return function(...args) {
const context = this;
// 清除之前的定时器
if (timerId) {
clearTimeout(timerId);
}
// 如果没有微任务在排队,创建一个
if (!microtaskQueued) {
microtaskQueued = true;
queueMicrotask(() => {
microtaskQueued = false;
// 设置新的定时器
timerId = setTimeout(() => {
fn.apply(context, args);
timerId = null;
}, delay);
});
}
};
}
// 使用示例
const expensiveSearch = debounceMicrotask((query) => {
console.log('搜索:', query);
// 实际搜索逻辑
}, 300);
// 快速连续输入
expensiveSearch('a');
expensiveSearch('ab');
expensiveSearch('abc'); // 只执行最后一次
4.4 React中的批量更新
javascript
// React利用事件循环实现状态批量更新
class FakeReact {
constructor() {
this.state = {};
this.isBatchingUpdates = false;
this.pendingStates = [];
}
setState(newState) {
if (this.isBatchingUpdates) {
// 如果在批处理中,收集状态更新
this.pendingStates.push(newState);
} else {
// 否则直接更新
this.applyUpdate(newState);
}
}
batchedUpdates(callback) {
this.isBatchingUpdates = true;
try {
callback();
} finally {
this.isBatchingUpdates = false;
// 在微任务中执行所有收集的更新
if (this.pendingStates.length > 0) {
queueMicrotask(() => {
const states = [...this.pendingStates];
this.pendingStates = [];
states.forEach(state => {
this.applyUpdate(state);
});
});
}
}
}
applyUpdate(newState) {
this.state = { ...this.state, ...newState };
console.log('状态更新:', this.state);
}
}
// 使用示例
const react = new FakeReact();
react.batchedUpdates(() => {
react.setState({ count: 1 });
react.setState({ count: 2 });
react.setState({ count: 3 });
});
// 只会触发一次更新:{ count: 3 }
第五部分:常见陷阱与最佳实践
5.1 微任务无限递归
javascript
// 危险的代码:微任务无限循环
function dangerousMicrotaskLoop() {
Promise.resolve().then(() => {
console.log('微任务执行');
dangerousMicrotaskLoop(); // 递归调用
});
}
// 这会阻塞事件循环,导致页面无响应
// 因为微任务队列永远不会清空
// 安全的方式:使用宏任务
function safeMacrotaskLoop() {
console.log('宏任务执行');
setTimeout(safeMacrotaskLoop, 0); // 允许渲染
}
5.2 混合使用宏任务和微任务
javascript
// 不推荐的模式
button.addEventListener('click', () => {
// 宏任务中产生微任务
Promise.resolve().then(() => {
// 微任务中又产生宏任务
setTimeout(() => {
// 难以追踪执行顺序
console.log('多层嵌套');
}, 0);
});
});
// 推荐的模式:保持清晰的任务层次
async function handleClick() {
// 步骤1:微任务处理
await processImmediate();
// 步骤2:宏任务处理
setTimeout(() => {
processDelayed();
}, 0);
}
button.addEventListener('click', handleClick);
5.3 最佳实践总结
-
优先使用微任务:对于需要立即执行但不阻塞渲染的任务
-
适时使用宏任务:对于可以延迟执行或需要允许渲染的任务
-
避免微任务递归:防止微任务队列永不空
-
合理使用async/await:理解其基于Promise(微任务)的本质
-
考虑使用queueMicrotask:比Promise.resolve().then()更语义化
-
注意执行顺序:在混合使用时要清晰了解执行顺序
第六部分:现代API与事件循环
6.1 requestAnimationFrame
javascript
// requestAnimationFrame在渲染前执行
console.log('开始');
setTimeout(() => {
console.log('setTimeout');
}, 0);
requestAnimationFrame(() => {
console.log('requestAnimationFrame');
});
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('结束');
// 典型输出:开始 → 结束 → Promise → requestAnimationFrame → setTimeout
// 但注意:RAF在渲染前执行,时机可能因浏览器而异
6.2 requestIdleCallback
javascript
// 在空闲时间执行低优先级任务
function processIdleTasks(deadline) {
while (tasks.length > 0 && deadline.timeRemaining() > 0) {
const task = tasks.shift();
task();
}
if (tasks.length > 0) {
requestIdleCallback(processIdleTasks);
}
}
// 与事件循环的配合
button.addEventListener('click', () => {
// 高优先级任务立即执行
console.log('点击处理');
// 低优先级任务在空闲时执行
requestIdleCallback(() => {
console.log('空闲任务');
});
});
6.3 MutationObserver
javascript
// MutationObserver使用微任务
const observer = new MutationObserver((mutations) => {
console.log('DOM变化', mutations);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// 测试
setTimeout(() => {
document.body.appendChild(document.createElement('div'));
console.log('添加元素后');
}, 0);
// 输出顺序:添加元素后 → DOM变化
// MutationObserver回调作为微任务执行
总结:掌握事件循环的艺术
事件循环是JavaScript异步编程的核心机制,理解宏任务和微任务的差异对于编写高性能、响应迅速的前端应用至关重要。
关键要点:
-
宏任务是独立的,执行完一个宏任务后,会执行所有微任务
-
微任务是紧接的,在当前宏任务结束后立即执行
-
渲染时机:通常在微任务执行完毕后,下一个宏任务开始前
-
优先级:同步代码 > 微任务 > 渲染 > 宏任务
何时使用什么?
-
微任务:需要立即执行的状态更新、Promise处理、数据同步
-
宏任务:需要延迟执行的任务、I/O操作、用户交互处理
-
requestAnimationFrame:与渲染相关的动画、视觉更新
-
requestIdleCallback:低优先级的后台任务
记住:事件循环不是JavaScript引擎的特性,而是宿主环境(浏览器/Node.js)提供的机制。不同的宿主环境可能有不同的实现,但核心概念相通。
通过深入理解事件循环,你不仅能写出更好的异步代码,还能更有效地调试性能问题,构建更流畅的用户体验。