JavaScript通过事件循环实现异步非阻塞,执行栈为空时先清空微任务队列再取宏任务;例如console.log同步执行,Promise.then入微任务,setTimeout入宏任务,输出顺序为1→4→3→2。

JavaScript 是单线程语言,意味着它在同一时间只能执行一个任务。为了处理异步操作(比如定时器、网络请求、DOM 事件等)而不阻塞主线程,JS 引入了事件循环(Event Loop)机制。理解事件循环是掌握异步编程的关键。
1. 执行栈与任务队列
JS 的代码执行依赖于执行栈(Call Stack),它是一个后进先出的结构,用于追踪函数的调用顺序。当函数被调用时,它会被压入栈中;执行完毕后,从栈中弹出。
然而,像 setTimeout、fetch 或 addEventListener 这类异步操作并不会立即进入执行栈。它们会被交给浏览器的其他模块(如定时器模块、HTTP 模块)处理,完成后将对应的回调函数推入任务队列(Task Queue)。
常见的任务队列分为:
立即学习“Java免费学习笔记(深入)”;
- 宏任务队列(Macro Task):包括整体代码块、setTimeout、setInterval、I/O、UI 渲染等。
- 微任务队列(Micro Task):包括 Promise.then、MutationObserver、queueMicrotask 等。
2. 事件循环的工作流程
事件循环的核心职责是不断检查执行栈是否为空,一旦为空,就从任务队列中取出最早的任务推入执行栈执行。但它有一个优先级规则:
- 每次执行完一个宏任务后,会立刻清空当前所有的微任务。
- 微任务执行期间产生的新微任务,也会被加入微任务队列并被执行。
- 微任务队列清空后,才开始下一轮事件循环,取下一个宏任务。
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
输出顺序是:1 → 4 → 3 → 2。因为:
- “1” 和 “4” 是同步代码,直接执行。
- setTimeout 回调进入宏任务队列。
- Promise.then 进入微任务队列。
- 同步代码执行完后,事件循环先处理微任务(输出3),再处理宏任务(输出2)。
3. 宏任务与微任务的实际影响
理解两者的执行时机有助于避免一些异步陷阱。例如:
Promise.resolve().then(() => {
console.log('微任务1');
process.nextTick(() => console.log('Node中的微任务'));
Promise.resolve().then(() => console.log('嵌套微任务'));
});
console.log('同步任务');
在 Node.js 环境中,输出为:同步任务 → 微任务1 → 嵌套微任务 → Node中的微任务。这说明微任务会按队列顺序执行,且嵌套的微任务也会被追加并及时处理。
在浏览器中,queueMicrotask 提供了一种显式添加微任务的方式,常用于延迟执行但又希望快于下一轮渲染的操作。
4. 与浏览器渲染的关系
浏览器的 UI 渲染也是一个宏任务。通常,每轮事件循环结束后,如果需要更新界面,浏览器会在合适的时机进行渲染。但由于微任务会在渲染前全部执行完,因此长时间运行的微任务会阻塞页面渲染。
所以,大量异步更新应优先使用宏任务(如 setTimeout)来让出控制权,避免界面卡顿。
基本上就这些。事件循环虽小,却是异步行为的基石。搞清楚宏任务和微任务的执行顺序,能让你写出更可预测的 JavaScript 代码。不复杂,但容易忽略细节。











