FumadocsZDecode
JavaScript

事件循环

JavaScript 是单线程语言,同一时刻只能执行一件事。事件循环(Event Loop)是浏览器和 Node.js 协调异步任务的核心机制,它决定了代码的实际执行顺序。


一、浏览器中的事件循环

执行模型

JavaScript 将任务分为两类:

  • 同步任务:进入主调用栈,立即按顺序执行。
  • 异步任务:交给浏览器 API 处理(定时器、网络请求、DOM 事件等),完成后将回调放入对应队列,等待执行。

两类任务队列

类型描述常见来源
宏任务(Macro Task)主体任务,每次事件循环执行一个setTimeoutsetInterval、UI 事件、MessageChannel
微任务(Micro Task)每个宏任务结束后立即全部清空,优先级更高Promise.thenqueueMicrotaskMutationObserver

执行顺序

执行同步代码(调用栈)

清空全部微任务队列

执行一个宏任务

清空全部微任务队列

执行下一个宏任务 ...(如此循环)

示例

console.log('同步 1')

setTimeout(() => {
  console.log('宏任务 1')
  Promise.resolve().then(() => {
    console.log('宏任务 1 产生的微任务')
  })
}, 0)

Promise.resolve().then(() => {
  console.log('微任务 1')
  setTimeout(() => {
    console.log('微任务产生的宏任务')
  }, 0)
})

console.log('同步 2')

// 输出顺序:
// 同步 1
// 同步 2
// 微任务 1
// 宏任务 1
// 宏任务 1 产生的微任务
// 微任务产生的宏任务

二、Node.js 中的事件循环

Node.js 的事件循环基于 libuv,分为六个阶段,依次循环执行:

阶段说明
timers执行 setTimeout / setInterval 到期的回调
pending callbacks执行上一轮延迟的 I/O 错误回调
idle / prepare内部使用
poll获取新的 I/O 事件,执行 I/O 相关回调
check执行 setImmediate 回调
close callbacks执行关闭事件回调,如 socket.on('close', ...)

Node.js 还提供了 process.nextTick(),它的优先级高于所有微任务,在当前操作完成后、进入下一阶段前立即执行:

process.nextTick(() => console.log('nextTick'))
Promise.resolve().then(() => console.log('Promise'))
setTimeout(() => console.log('setTimeout'), 0)

// 输出顺序:
// nextTick
// Promise
// setTimeout

在 I/O 回调内部,setImmediate 总是比 setTimeout 先执行:

const fs = require('fs')

fs.readFile('./file.txt', () => {
  setTimeout(() => console.log('timeout'), 0)
  setImmediate(() => console.log('immediate'))
})

// 在 I/O 回调中输出顺序:
// immediate
// timeout

三、浏览器与 Node.js 的区别

对比点浏览器Node.js
微任务清空时机每个宏任务执行完后立即清空每个阶段结束后清空
最高优先级异步queueMicrotask / Promise.thenprocess.nextTick
额外 APIrequestAnimationFramesetImmediateprocess.nextTick
// 浏览器:Promise 微任务先于 setTimeout 宏任务
Promise.resolve().then(() => console.log('Promise'))
setTimeout(() => console.log('setTimeout'), 0)
// 输出:Promise → setTimeout

// Node.js:process.nextTick 优先级最高
process.nextTick(() => console.log('nextTick'))
Promise.resolve().then(() => console.log('Promise'))
// 输出:nextTick → Promise

四、定时器的调度机制

每一轮事件循环中,浏览器检查 setTimeout 注册时间加上 delay 是否已到期,到期才将其回调放入宏任务队列。delay0 并不代表"立即执行",而是"尽早执行"。

const fakeAjax = (delay = 1000) =>
  new Promise(resolve => {
    setTimeout(() => {
      console.log('sleep', delay)
      resolve()
    }, delay)
  })

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

fakeAjax() // delay 1000ms

setTimeout(() => console.log('setTimeout 100'), 100)

// 输出顺序:
// setTimeout 0   (最早到期)
// setTimeout 100
// sleep 1000

On this page