题目
事件循环与任务队列执行顺序分析
信息
- 类型:问答
- 难度:⭐⭐
考点
事件循环机制,宏任务与微任务,异步代码执行顺序
快速回答
当浏览器执行以下代码时:
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'
核心原因:
- 同步代码立即执行
- 微任务(Promise)优先于宏任务(setTimeout)执行
- 事件循环按阶段处理任务队列
原理说明
浏览器事件循环(Event Loop)是协调同步/异步任务的核心机制:
- 调用栈(Call Stack):同步代码立即执行,形成执行栈
- 任务队列(Task Queues):
- 微任务队列(Microtask Queue):Promise.then、MutationObserver、queueMicrotask
- 宏任务队列(Macrotask Queue):setTimeout、setInterval、DOM事件、I/O
- 执行流程:
- 执行同步代码(调用栈清空)
- 执行所有微任务(直到队列清空)
- 渲染页面(如需要)
- 执行一个宏任务
- 重复步骤2-4
代码示例分析
console.log('script start'); // 1. 同步任务
setTimeout(() => {
console.log('setTimeout'); // 5. 宏任务
}, 0);
Promise.resolve()
.then(() => {
console.log('promise1'); // 3. 微任务
})
.then(() => {
console.log('promise2'); // 4. 微任务(链式调用)
});
console.log('script end'); // 2. 同步任务执行步骤分解:
- 同步代码顺序执行,输出 'script start' 和 'script end'
- 调用栈清空后,检查微任务队列
- 执行 Promise.then 回调:输出 'promise1',并添加新微任务(第二个 then)
- 继续清空微任务队列:输出 'promise2'
- 执行下一个宏任务(setTimeout回调):输出 'setTimeout'
最佳实践
- 性能优化:耗时操作用微任务拆分,避免阻塞渲染
// 将长任务拆分为微任务队列 function processChunk() { // ...处理数据块 if (hasMore) queueMicrotask(processChunk); } queueMicrotask(processChunk); - 执行顺序控制:关键操作使用微任务确保优先执行
- 避免嵌套过深:微任务队列连续执行可能导致阻塞
常见错误
- 误判执行顺序:认为 setTimeout(0) 会立即执行
- 微任务递归:微任务中递归添加微任务导致死循环
// 错误示例:阻塞主线程 function recursiveMicrotask() { Promise.resolve().then(recursiveMicrotask); } recursiveMicrotask(); - 混用任务类型:未考虑 requestAnimationFrame 在渲染阶段的特殊性
扩展知识
- Node.js vs 浏览器:Node.js 中 process.nextTick 优先级高于微任务
- requestAnimationFrame:在渲染前执行,适合动画更新
- 任务优先级:
- 同步代码 > process.nextTick(Node) > 微任务 > 渲染 > 宏任务
- 性能监测:使用 Performance API 分析任务耗时
performance.mark('start'); // 执行代码 performance.mark('end'); performance.measure('task', 'start', 'end');