事件循环: 宏任务和微任务

122 阅读3分钟

事件循环

事件循环(Event Loop)是JavaScript实现异步编程的核心机制.由于JavaScript是单线程的语言,事件循环机制使得他能够非阻塞的处理各种异步操作,如用户交互/网络请求/定时器等.

事件循环的基本流程

  1. 执行同步代码 : 所有同步任务在主线程上顺序执行
  2. 处理微任务 : 执行当前微任务队列中的所有任务
  3. 渲染页面 : 执行UI渲染
  4. 处理宏任务 : 从宏任务队列中取出一个任务执行
  5. 循环 : 重复以上过程

宏任务和微任务

宏任务

宏任务代表一些离散的,独立的工作单元.浏览器会在每个宏任务之间进行页面渲染.

常见的宏任务源包括:

  • setTimeoutsetInterval 回调
  • I/O 操作(如文件读取)
  • UI渲染
  • 用户交互事件(如点击,滚动)
  • postMessage
  • MessageChannel
  • requestAnimationFrame(存在争议, 有些浏览器将其视为微任务)

微任务

微任务是更小的任务,在当前宏任务执行完成之后立刻执行,且在页面渲染之前.

常见的微任务源包括:

  • Promisethen,catch,finally回调
  • MutationObserver回调
  • queueMicrotask API
  • process.nextTick (Node.js 环境)

执行顺序示例

console.log('game start')

setTimeout(() => console.log('STO'), 0)

Promise.resolve().then(() => console.log('Promise1')).then(() => console.log('Promise2'))

console.log('end')

输出顺序

game start
end
Promise1
Promise2
STO

详细执行过程分析

  1. 执行同步代码:
  • 输出 'game start'
  • 设置一个定时器(在 0 ms后回调加入宏任务队列)
  • 创建一个已解决的Promise, 将其then 加入微任务队列
  • 输出 'end'
  1. 同步代码执行完毕,检查微任务队列
  • 执行第一个then回调, 输出Promise1
  • 该回调返回undefined, 创建一个新的已解决的Promise, 将第二个then加入微任务队列
  • 执行第二个then回调, 输出Promise2
  1. 微任务队列为空,检查是否需要渲染
  2. 从宏任务队列中取出setTimeout回调并执行, 输出 STO

事件循环和渲染的关系

浏览器通常会在每个宏任务之间进行页面渲染,但在执行微任务队列时不会渲染. 这意味着:

  • 微任务会在当前宏任务结束后立即执行, 且会阻塞渲染
  • 大量微任务可能会导致页面 '卡死', 因为没有机会渲染
// 这个例子会阻塞页面
function loop(){
    Promise.resolve().then(loop)
}

loop()

Node.js与浏览器的事件循环差异

Node.js的事件循环与浏览器有一些不同:

  1. Node.js有多个阶段(timers, pending callbacks, idle / prepare, poll, check, close callbacks)
  2. process.nextTick 比微任务的优先级更高
  3. setImmediate 是 Node特有的, 在当前事件循环结束时执行

实际使用建议

  1. 长时间运行的任务: 分解为多个小任务或用 setTimeout / setImmediate 让出控制权
  2. 优先级控制: 使用微任务处理高优先级回调
  3. 避免微任务爆炸: 不要在一个微任务中创建太多新的微任务