JavaScript 宏任务与微任务详解
1. 基本概念
宏任务(MacroTask)和微任务(MicroTask)是 JavaScript 事件循环机制中的两个核心概念,用于管理异步任务的执行顺序。
1.1 宏任务
宏任务是由宿主环境(如浏览器、Node.js)发起的异步任务,具有以下特点:
- 执行优先级低于微任务
- 在下一轮事件循环中执行
- 常见的宏任务包括:
- setTimeout/setInterval
- setImmediate(Node.js环境)
- requestAnimationFrame(浏览器环境)
- I/O操作
- UI渲染事件
- script标签中的整体代码
- DOM事件回调
1.2 微任务
微任务是由 JavaScript 引擎自身发起的异步任务,具有以下特点:
- 执行优先级高于宏任务
- 在当前宏任务执行完毕后立即执行
- 常见的微任务包括:
- Promise.then/catch/finally
- MutationObserver
- process.nextTick(Node.js环境)
- queueMicrotask
- Object.observe(已废弃)
2. 执行机制与顺序
2.1 事件循环流程
JavaScript 引擎通过事件循环(Event Loop)来管理宏任务和微任务的执行,具体流程如下:
- 执行同步代码(属于宏任务)
- 执行所有微任务队列中的任务(清空微任务队列)
- 执行渲染操作(如果需要)
- 从宏任务队列中取出一个任务执行
- 重复步骤2-4
2.2 批量更新机制
批量更新是利用宏任务和微任务特性实现的一种优化策略,主要用于:
- 减少不必要的DOM操作和渲染
- 提高应用性能
- 确保数据一致性
其核心思想是:将多次连续的更新操作合并为一次,在适当时机批量执行。
3. Vue源码中的应用
3.1 Vue异步更新队列实现原理
Vue 利用宏任务和微任务机制实现了高效的异步更新队列,主要通过 nextTick 方法实现:
3.1.1 核心实现逻辑
- 队列收集:当修改响应式数据时,Vue 会将需要更新的组件标记为脏,并将更新操作推入异步队列
- 异步执行:Vue 优先使用微任务(Promise.then),降级使用宏任务(setTimeout)
- 批量更新:多次修改数据只会触发一次实际的DOM更新
3.1.2 关键代码逻辑
javascript
// nextTick 核心实现逻辑
const callbacks = []
let pending = false
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 优先使用微任务,降级使用宏任务
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 优先使用 Promise.then(微任务)
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
} else if (typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')) {
// 使用 MutationObserver(微任务)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 使用 setImmediate(宏任务)
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 最后降级使用 setTimeout(宏任务)
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// 支持 Promise 链式调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}3.2 实际应用场景
在 Vue 中,异步更新队列的应用场景包括:
- 数据修改后获取更新后的 DOM
- 避免频繁 DOM 更新导致的性能问题
- 确保数据驱动视图的一致性
4. 其他框架/库中的应用
4.1 React 中的应用
React 利用事件循环机制和微任务来处理状态更新和批处理:
- React 的状态更新在同一个事件处理函数中会被合并(批量更新)
- React 18 引入了自动批处理机制,在更多场景下自动合并更新
- 使用
flushSync可以强制同步执行更新(不推荐频繁使用)
4.2 Node.js 中的应用
Node.js 中的事件循环与浏览器略有不同,它将事件循环分为多个阶段:
- Timers 阶段:执行 setTimeout 和 setInterval 的回调
- Pending Callbacks 阶段:执行延迟的 I/O 回调
- Idle, Prepare 阶段:内部使用
- Poll 阶段:执行 I/O 回调
- Check 阶段:执行 setImmediate 的回调
- Close Callbacks 阶段:执行关闭事件的回调
在每个阶段执行完毕后,Node.js 会先清空所有微任务队列,然后再进入下一个阶段。
4.3 设计模式应用
宏任务和微任务的特性被广泛应用于以下设计模式:
- 任务队列模式:实现异步任务的有序执行
- 发布-订阅模式:确保订阅者在适当时机接收到通知
- 批处理模式:合并多个操作以提高性能
- 调度器模式:控制任务的执行时机和优先级
5. 实际应用技巧
5.1 性能优化策略
- 使用微任务进行即时更新:对于需要在当前事件循环中完成的操作,优先使用微任务
- 使用宏任务避免阻塞:对于耗时较长的操作,使用宏任务避免阻塞UI渲染
- 合理利用批量更新:将多次DOM操作合并,减少渲染次数
5.2 常见应用场景
- 数据获取与渲染分离:使用微任务确保数据完全获取后再渲染
- 动画与交互优化:结合 requestAnimationFrame(宏任务)和微任务实现流畅动画
- 错误处理与恢复:使用微任务处理异步操作中的错误,避免影响主流程
- 复杂状态管理:利用任务队列管理复杂的状态更新依赖关系
6. 面试题与核心思想
6.1 经典面试题
面试题1:分析以下代码的输出顺序
javascript
console.log('1');
s setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
}, 0);
});
console.log('6');答案:1 6 4 2 3 5
解析:
- 先执行同步代码:输出 1 和 6
- 执行微任务:Promise.then 输出 4,并注册一个宏任务
- 执行宏任务:第一个 setTimeout 输出 2,并注册一个微任务
- 执行微任务:新注册的 Promise.then 输出 3
- 执行宏任务:第二个 setTimeout 输出 5
面试题2:Vue 中为什么要用 nextTick?
答案:
- Vue 使用异步更新队列,当数据变化时,DOM 更新不会立即执行
- nextTick 可以在下次 DOM 更新循环结束后执行回调,确保能获取到更新后的 DOM
- 这是基于性能考虑,Vue 会将多次数据修改合并为一次 DOM 更新
面试题3:微任务和宏任务的区别是什么?
答案:
- 执行时机:微任务在当前宏任务执行完毕后立即执行,宏任务在下一轮事件循环中执行
- 优先级:微任务优先级高于宏任务
- 来源:微任务由 JavaScript 引擎发起,宏任务由宿主环境发起
- 执行次数:每轮事件循环中,微任务队列会被清空,而宏任务队列只执行一个
6.2 核心思想记忆口诀
宏任务微任务执行顺序口诀
"同微宏,微先清,宏单执行再循环"
- 同:先执行同步代码
- 微:然后执行所有微任务
- 宏:接着执行宏任务
- 微先清:微任务队列要全部清空
- 宏单执行:宏任务每次只执行一个
- 再循环:完成后进入下一轮循环
宏任务微任务分类口诀
"宏宿主,微引擎,认清单双要记清"
- 宏宿主:宏任务由宿主环境(浏览器/Node)创建
- 微引擎:微任务由 JavaScript 引擎创建
- 认清单双:能分清哪些API属于宏任务,哪些属于微任务
Vue nextTick 实现口诀
"优先微,降级宏,队列收集批量更"
- 优先微:优先使用微任务(Promise/MutationObserver)
- 降级宏:降级使用宏任务(setImmediate/setTimeout)
- 队列收集:将多个更新回调收集到队列中
- 批量更:一次性批量执行所有更新
7. 总结
宏任务和微任务是 JavaScript 异步编程的核心概念,理解它们的执行机制对于编写高效、可靠的异步代码至关重要。通过合理利用宏任务和微任务,我们可以优化应用性能,改善用户体验,并避免常见的异步编程陷阱。
在实际开发中,应根据具体场景选择合适的任务类型:需要立即执行的操作使用微任务,需要延迟执行或避免阻塞的操作使用宏任务。同时,也要理解各框架如何利用这些机制来实现其核心功能,这有助于我们更好地使用这些框架并进行性能优化。