JS事件循环机制_深入理解JavaScript异步编程

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

JavaScript 是单线程语言,意味着它在同一时间只能执行一个任务。为了处理异步操作(比如定时器、网络请求、DOM 事件等)而不阻塞主线程,JS 引入了事件循环(Event Loop)机制。理解事件循环是掌握异步编程的关键。

1. 执行栈与任务队列

JS 的代码执行依赖于执行栈(Call Stack),它是一个后进先出的结构,用于追踪函数的调用顺序。当函数被调用时,它会被压入栈中;执行完毕后,从栈中弹出。

然而,像 setTimeoutfetchaddEventListener 这类异步操作并不会立即进入执行栈。它们会被交给浏览器的其他模块(如定时器模块、HTTP 模块)处理,完成后将对应的回调函数推入任务队列(Task Queue)。

常见的任务队列分为:

  • 宏任务队列(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 代码。不复杂,但容易忽略细节。