你以为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 → queueMicrotask2.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 → 宏任务 24.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)提供的机制。不同的宿主环境可能有不同的实现,但核心概念相通。
通过深入理解事件循环,你不仅能写出更好的异步代码,还能更有效地调试性能问题,构建更流畅的用户体验。