Skip to content

事件循环与任务队列

JavaScript 是单线程语言,但能处理异步操作——这一切都依赖事件循环(Event Loop)机制。理解它是掌握 JS 异步编程的基础。

执行模型概览

┌─────────────────────────────────────────┐
│              Call Stack(调用栈)         │
│  [ main() → setTimeout cb → ... ]       │
└──────────────────┬──────────────────────┘
                   │ 栈空时取任务
        ┌──────────▼──────────┐
        │    Event Loop        │
        └──────────┬──────────┘
         ┌─────────┴──────────┐
         ▼                    ▼
  Microtask Queue      Macrotask Queue
  (微任务队列)          (宏任务队列)
  - Promise.then       - setTimeout
  - queueMicrotask     - setInterval
  - MutationObserver   - setImmediate (Node)
                       - I/O callbacks
                       - requestAnimationFrame

执行顺序规则

每轮 Event Loop 的执行顺序:

  1. 执行当前调用栈中的同步代码(直到栈空)
  2. 清空所有微任务(Microtask Queue)
  3. 取一个宏任务(Macrotask)执行
  4. 再次清空所有微任务
  5. 重复 3-4

关键点

微任务队列会在每个宏任务执行完后完全清空,然后才执行下一个宏任务。

经典面试题解析

js
console.log('1')                          // 同步

setTimeout(() => console.log('2'), 0)     // 宏任务

Promise.resolve()
  .then(() => console.log('3'))           // 微任务
  .then(() => console.log('4'))           // 微任务

console.log('5')                          // 同步

// 输出顺序:1 → 5 → 3 → 4 → 2

执行过程分解:

同步阶段:
  console.log('1')  ✓
  setTimeout 注册到宏任务队列
  Promise.then 注册到微任务队列
  console.log('5')  ✓

微任务清空:
  console.log('3')  ✓
  .then(() => '4') 注册到微任务队列
  console.log('4')  ✓

宏任务:
  console.log('2')  ✓

async/await 的本质

async/await 是 Promise 的语法糖,await 后面的代码等价于 .then() 回调:

js
async function foo() {
  console.log('A')
  await Promise.resolve()
  console.log('B')   // 等价于 Promise.resolve().then(() => console.log('B'))
  await Promise.resolve()
  console.log('C')
}

console.log('start')
foo()
console.log('end')

// 输出:start → A → end → B → C

更复杂的例子

js
async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')    // 微任务
}

async function async2() {
  console.log('async2')
}

console.log('script start')

setTimeout(() => console.log('setTimeout'), 0)  // 宏任务

async1()

new Promise(resolve => {
  console.log('promise1')
  resolve()
}).then(() => console.log('promise2'))           // 微任务

console.log('script end')

// 输出:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end    ← 微任务
// promise2      ← 微任务
// setTimeout    ← 宏任务

Node.js 的事件循环

Node.js 的事件循环基于 libuv,比浏览器多了几个阶段:

   ┌───────────────────────────┐
┌─>│           timers          │  ← setTimeout / setInterval
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  ← I/O 错误回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  ← 内部使用
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │  ← 获取新 I/O 事件
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │  ← setImmediate
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

process.nextTick vs Promise.then

js
// Node.js 中,process.nextTick 优先级高于 Promise.then
Promise.resolve().then(() => console.log('Promise'))
process.nextTick(() => console.log('nextTick'))

// 输出:nextTick → Promise
// nextTick 有独立的队列,在微任务队列之前执行

setImmediate vs setTimeout

js
// 在 I/O 回调中,setImmediate 总是先于 setTimeout
const fs = require('fs')

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

// 输出:immediate → timeout(在 I/O 回调中是确定的)

实战:避免阻塞事件循环

js
// ❌ 错误:长时间同步计算阻塞事件循环
function heavyCompute(n) {
  let result = 0
  for (let i = 0; i < n; i++) {
    result += Math.sqrt(i)
  }
  return result
}

// ✅ 正确:分片执行,让出控制权
function heavyComputeAsync(n, chunkSize = 10000) {
  return new Promise(resolve => {
    let result = 0
    let i = 0

    function chunk() {
      const end = Math.min(i + chunkSize, n)
      while (i < end) {
        result += Math.sqrt(i++)
      }
      if (i < n) {
        setTimeout(chunk, 0)  // 让出事件循环
      } else {
        resolve(result)
      }
    }

    chunk()
  })
}

微任务泄漏

js
// ⚠️ 危险:无限微任务会饿死宏任务
function infiniteMicrotask() {
  Promise.resolve().then(infiniteMicrotask)
}
infiniteMicrotask()
// setTimeout 的回调永远不会执行!

// ✅ 正确:用 setTimeout 打断
function safeRecursive() {
  setTimeout(safeRecursive, 0)  // 宏任务,不会饿死其他任务
}

可视化工具

推荐使用 Loupe 可视化观察 Event Loop 的执行过程。

总结

  • 同步代码 → 微任务(Promise/nextTick)→ 宏任务(setTimeout/I/O)
  • 每个宏任务执行完后,清空所有微任务
  • async/await 本质是 Promise,await 后的代码是微任务
  • Node.js 的 process.nextTick 优先级高于 Promise.then

系统学习 Web 前端生态,深入底层架构