Skip to content

事件循环

什么是事件循环?

概括

是一种任务调度机制。

就是 JavaScript 单线程模型下,用来协调同步代码、异步任务执行顺序的一种机制。

就是 先执行所有同步代码,再清空所有微任务,再执行一个宏任务,然后重复“清空微任务 → 执行一个宏任务”。

详解

因为 JS 是单线程的,一次只能执行一个任务。如果所有任务都按顺序同步执行,那么遇到网络请求、定时器这种耗时操作,页面就会卡住。

所以 JS 把任务分成了两类:同步任务和异步任务。同步任务直接进调用栈执行,异步任务会先放到任务队列里等。

异步任务

异步任务分宏任务和微任务。

宏任务包括 setTimeout、setInterval、I/O、UI 渲染等。

微任务包括 Promise.then、queueMicrotask、MutationObserver 等。

完整流程

  1. 执行当前调用栈中所有的同步代码,直到调用栈清空
  2. 清空整个微任务队列(逐个取出微任务放入调用栈执行,执行过程中产生的新微任务追加到队尾,继续在本轮清空)
  3. 从宏任务队列中取出一个宏任务,放入调用栈执行
  4. 该宏任务执行完后,调用栈再次清空
  5. 回到第 2 步,继续清空微任务队列
  6. 再回到第 3 步,取下个宏任务
  7. 如此循环

补充:

  • 清空微任务队列,把微任务一个一个拿到调用栈执行。产生的新的微任务放到微任务队列末尾,并再本轮清空。产生的同步代码,在栈里直接执行。
  • 每一步执行完(无论是同步初始、微任务、宏任务),调用栈都会回到空状态,然后事件循环决定下一步做什么。
  • 每一次取任务之前,调用栈一定是空的,这是事件循环保证的。

优先级

宏任务一般来自外部,比如 setTimeout、setInterval、I/O 操作(如 XHR 回调、文件读取)、UI 渲染、用户事件,它的特点是不可预测、与当前代码逻辑无关、执行时间可能很长。 微任务一般来自代码内部,比如 Promise.then、queueMicrotask、MutationObserver,它的特点是可预测、与当前代码逻辑连续、执行时间通常很短。

基于这两类任务的特性,事件循环的设计是:微任务优先级更高,每次宏任务执行完后会立刻清空所有微任务,这样能保证代码逻辑的连贯性(不被打断);宏任务每次只取一个,这样能防止多个宏任务连续执行导致微任务和页面渲染被长期阻塞。

rAF

requestAnimationFrame

既不属于宏任务,也不属于微任务。它属于 ‘渲染前回调’,存储在与任务队列并列的 ‘动画回调队列’ 中。

具体执行时,它不参与宏任务和微任务的排队,而是在事件循环的渲染阶段,位于‘清空微任务’之后、‘样式计算与绘制’之前,被一次性全部执行。

可以把 rAF 理解为帧回调。它的优先级高于宏任务,但低于微任务,因为它总是能插队在下一个宏任务之前执行,但又无法像微任务那样在当前任务中无限插队。

新标准

宏任务这个叫法不再精确,取而代之的是任务队列被拆分成了多个独立的任务源

多个任务源,各有独立的队列,浏览器会按优先级决定从哪个队列取任务。

优先级在于插队权,而不是先后顺序。

如果一个循环里要进行渲染,那么 rAF 一定会插进来。

js
// 旧理解:只有一个队列,谁先来谁先执行
queue: [setTimeout回调, 点击回调, fetch回调]
执行顺序:按入队顺序

// 新标准:多个队列,先看优先级
交互队列: [点击回调]      ← 优先级最高,先执行
网络队列: [fetch回调]      ← 优先级中等
定时器队列: [setTimeout回调] ← 优先级最低,最后执行
执行顺序:交互 → 网络 → 定时器(而不是按入队时间)