深入解析JavaScript单线程模型下的事件循环机制,掌握宏任务和微任务的执行顺序
引言:理解JavaScript的单线程本质
JavaScript作为一门单线程语言,意味着它一次只能执行一个任务。这种设计简化了编程模型,但也带来了挑战:如何处理耗时操作(如网络请求、定时器)而不阻塞主线程?事件循环(Event Loop) 就是解决这一问题的核心机制!
本文将带你深入探索事件循环的工作原理,特别是宏任务和微任务的区别与执行顺序,并通过多个实际示例加深理解。
核心概念解析
1. 事件循环的基本流程
- 执行一个宏任务(如script脚本)
- 执行所有微任务
- 进行页面渲染(如果有必要)
- 开始下一个宏任务
2. 宏任务与微任务的区别
| 宏任务 | 微任务 |
|---|---|
| setTimeout | Promise.then |
| setInterval | queueMicrotask |
| I/O操作 | MutationObserver |
| UI渲染 | process.nextTick(Node) |
| script整体代码 |
实战示例分析
示例1:基础执行顺序
<script>
console.log('script start'); // 同步任务
setTimeout(() => {
console.log('setTimeout'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('promise'); // 微任务
});
console.log('script end'); // 同步任务
</script>
执行顺序分析:
- 同步任务:
script start→script end - 微任务:
promise - 宏任务:
setTimeout
输出结果:
script start
script end
promise
setTimeout
示例2:MutationObserver微任务
<script>
const target = document.createElement('div');
document.body.appendChild(target);
const observer = new MutationObserver(() => {
console.log('微任务: MutaionObserver');
});
observer.observe(target, { attributes: true });
// 连续修改DOM
target.setAttribute('data-set', '123');
target.appendChild(document.createTextNode('123'));
target.setAttribute('style', 'background-color: green;');
</script>
关键点:
- 多个DOM修改会被合并为一次微任务执行
- MutationObserver回调在渲染前执行,可以获取DOM最新状态
示例3:Node.js中的微任务(3.js)
console.log('Start');
process.nextTick(() => {
console.log('Process Next Tick'); // Node特有微任务
});
Promise.resolve().then(() => {
console.log('Promise Resolved');
});
setTimeout(() => {
console.log('haha');
Promise.resolve().then(() => {
console.log('inner Promise')
})
}, 0);
console.log('end');
Node环境输出:
Start
end
Process Next Tick
Promise Resolved
haha
inner Promise
注意: Node.js中process.nextTick优先级高于Promise微任务
示例4:批量Promise处理
console.log('同步Start');
const promise1 = Promise.resolve('First Promise');
const promise2 = Promise.resolve('Secend Promise');
const promise3 = new Promise(resolve => {
console.log('Promise3'); // 同步执行
resolve('Third Promise');
});
// 批量添加then处理
promise1.then(console.log);
promise2.then(console.log);
promise3.then(console.log);
setTimeout(console.log, 0, '下一把再见');
// 重复添加
promise1.then(console.log);
promise2.then(console.log);
promise3.then(console.log);
console.log('同步end');
执行顺序:
- 同步代码
- 所有Promise.then微任务(按添加顺序)
- setTimeout宏任务
示例5:queueMicrotask API
<script>
console.log('同步');
queueMicrotask(() => {
console.log('微任务:queueMicrotask');
});
console.log('同步结束');
</script>
现代浏览器支持: 提供标准API直接创建微任务
事件循环流程图解
最佳实践与性能优化
-
长任务分解: 使用微任务拆分耗时操作
function processChunk() { // 处理数据块 if (hasMore) { queueMicrotask(processChunk); } } queueMicrotask(processChunk); -
避免微任务嵌套: 防止微任务无限循环导致页面卡死
-
合理使用宏任务:
// 需要等待UI更新后执行的操作 function afterRender() { requestAnimationFrame(() => { setTimeout(doSomethingAfterRender, 0); }); }
总结
理解事件循环机制是掌握JavaScript异步编程的关键:
- 同步任务优先执行
- 微任务在同步任务之后、渲染之前执行
- 宏任务在每轮事件循环最后执行
- 合理利用微任务可以提高应用响应速度
- 避免在微任务中进行大量计算阻塞渲染