Skip to content

事件循环 Event Loop 面试题全解析

一、核心要点速览

💡 核心考点

  • 执行栈: 同步任务的执行环境(LIFO)
  • 任务队列: 异步任务的等待队列(FIFO)
  • 宏任务: setTimeout、setInterval、I/O 等
  • 微任务: Promise.then、MutationObserver 等
  • 执行顺序: 同步 → 微任务 → 宏任务

二、JavaScript 运行时环境

1. 单线程模型

javascript
// JavaScript 是单线程语言
// 同一时间只能执行一个任务

console.log('start')

setTimeout(() => {
  console.log('timeout')
}, 0)

Promise.resolve().then(() => {
  console.log('promise')
})

console.log('end')

// 输出顺序:
// start
// end
// promise
// timeout

2. 为什么需要事件循环

┌──────────────────────────────────────────────────────────┐
│              为什么需要 Event Loop                        │
└──────────────────────────────────────────────────────────┘

问题场景:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
如果 JavaScript 没有事件循环:

用户点击按钮


┌─────────────────┐
│ 处理点击事件     │ ← 耗时 3 秒
└────────┬────────┘


    界面卡死 3 秒!


    ❌ 无法响应其他操作
    ❌ 无法滚动
    ❌ 无法输入

解决方案:Event Loop
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
用户点击按钮


┌─────────────────┐
│ 注册回调函数     │ ← 立即完成
│ 加入任务队列     │
└────────┬────────┘


    继续响应其他操作 ✓


    空闲时从队列取出执行 ✓

优势:
✓ 非阻塞 I/O
✓ 高并发能力
✓ 良好的用户体验
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

三、执行上下文与调用栈

1. 执行栈(Call Stack)

javascript
// 执行栈:后进先出(LIFO)

function first() {
  second()
}

function second() {
  third()
}

function third() {
  console.log('Hello')
}

first()

// 调用栈变化:
// ┌─────────────┐
// │ first()     │
// └─────────────┘
//      ↓
// ┌─────────────┐
// │ second()    │
// ├─────────────┤
// │ first()     │
// └─────────────┘
//      ↓
// ┌─────────────┐
// │ third()     │
// ├─────────────┤
// │ second()    │
// ├─────────────┤
// │ first()     │
// └─────────────┘
//      ↓
// 执行完成,全部弹出

2. 同步任务执行流程

时间 →  ─────────────────────────────────────────────────►

同步代码执行:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
源代码:
  console.log('A')
  console.log('B')
  console.log('C')

执行栈:
  ┌─────┐
  │  A  │ → 打印 A
  └─────┘

  ┌─────┐
  │  B  │ → 打印 B
  └─────┘

  ┌─────┐
  │  C  │ → 打印 C
  └─────┘

  空栈

输出:A → B → C
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

四、事件循环机制

1. Event Loop 完整流程

┌──────────────────────────────────────────────────────────┐
│                   Event Loop 完整流程                     │
└──────────────────────────────────────────────────────────┘

执行流程图:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    ┌─────────────────┐
    │  执行同步代码    │
    │  (调用栈)        │
    └────────┬────────┘


    ┌─────────────────┐
    │  调用栈为空?    │
    └────────┬────────┘

       ┌─────┴─────┐
       │           │
      是          否
       │           │
       ▼           │
┌──────────────┐   │
│ 执行所有微任务│   │
│(Microtasks)  │   │
└────────┬─────┘   │
         │         │
         ▼         │
┌──────────────┐   │
│ 渲染 DOM      │   │
│ (如果需要)    │   │
└────────┬─────┘   │
         │         │
         ▼         │
┌──────────────┐   │
│ 执行一个宏任务│   │
│(Macrotask)   │   │
└────────┬─────┘   │
         │         │
         └─────────┘


         回到开始

无限循环...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2. 任务队列详解

┌──────────────────────────────────────────────────────────┐
│                    任务队列结构                          │
└──────────────────────────────────────────────────────────┘

内存结构:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌────────────────────────────────────────────┐
│           JavaScript 运行时                 │
├────────────────────────────────────────────┤
│                                            │
│  ┌──────────────┐                         │
│  │   Call Stack │                         │
│  │   (调用栈)    │                         │
│  │              │                         │
│  │              │                         │
│  └──────────────┘                         │
│                                            │
│  ┌──────────────┐  ┌──────────────────┐   │
│  │ Microtask    │  │ Macrotask        │   │
│  │ Queue        │  │ Queue            │   │
│  │ (微任务队列)  │  │ (宏任务队列)      │   │
│  │              │  │                  │   │
│  │ • Promise    │  │ • setTimeout     │   │
│  │ • Mutation   │  │ • setInterval    │   │
│  │ • queueMicro │  │ • I/O            │   │
│  │              │  │ • UI 渲染         │   │
│  └──────────────┘  └──────────────────┘   │
│                                            │
└────────────────────────────────────────────┘

执行优先级:
1. 清空调用栈(同步代码)
2. 清空微任务队列
3. 渲染 DOM(如需要)
4. 执行一个宏任务
5. 重复步骤 1-4
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

五、宏任务 vs 微任务

1. 任务类型对比

javascript
// ========== 宏任务(Macrotask)==========

// 1. setTimeout / setInterval
setTimeout(() => {
  console.log('timeout')
}, 0)

// 2. setImmediate (Node.js)
setImmediate(() => {
  console.log('immediate')
})

// 3. I/O 操作
fs.readFile('file.txt', (err, data) => {
  console.log('file read')
})

// 4. UI 渲染
element.addEventListener('click', () => {
  console.log('click')
})

// 5. postMessage
window.postMessage('message', '*')


// ========== 微任务(Microtask)==========

// 1. Promise.then / catch / finally
Promise.resolve().then(() => {
  console.log('promise then')
})

// 2. queueMicrotask
queueMicrotask(() => {
  console.log('microtask')
})

// 3. MutationObserver
const observer = new MutationObserver(() => {
  console.log('DOM changed')
})

// 4. process.nextTick (Node.js)
process.nextTick(() => {
  console.log('next tick')
})

2. 详细对比表

特性宏任务(Macrotask)微任务(Microtask)
包含类型setTimeout、setInterval、I/O、UI 渲染Promise.then、queueMicrotask、MutationObserver
执行时机每次事件循环执行一个每轮事件循环清空所有
优先级
产生时间当前宏任务结束后当前任务结束后立即
数量每次一个一次性全部执行
典型应用延迟执行、定时器数据更新、DOM 批量修改

3. 执行顺序示例

javascript
console.log('script start')

setTimeout(() => {
  console.log('setTimeout 1')
  
  setTimeout(() => {
    console.log('setTimeout 2')
  }, 0)
  
  Promise.resolve().then(() => {
    console.log('promise in setTimeout')
  })
}, 0)

Promise.resolve().then(() => {
  console.log('promise 1')
  
  Promise.resolve().then(() => {
    console.log('promise 2')
  })
})

console.log('script end')

// 输出顺序:
// 1. script start         (同步)
// 2. script end           (同步)
// 3. promise 1            (微任务)
// 4. promise 2            (微任务)
// 5. setTimeout 1         (宏任务)
// 6. promise in setTimeout(微任务)
// 7. setTimeout 2         (宏任务)

六、经典面试题

题目 1:基础题

javascript
console.log('1')

setTimeout(() => {
  console.log('2')
}, 0)

Promise.resolve().then(() => {
  console.log('3')
})

console.log('4')

// 输出:1, 4, 3, 2
// 解析:
// 同步:1, 4
// 微任务:3
// 宏任务:2

题目 2:嵌套题

javascript
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')

// 输出顺序:
// 1. script start
// 2. async1 start
// 3. async2
// 4. promise1
// 5. script end
// 6. async1 end        (await 后的代码是微任务)
// 7. promise2
// 8. setTimeout

// 详细解析:
// 同步:script start, async1 start, async2, promise1, script end
// 微任务:async1 end, promise2
// 宏任务:setTimeout

题目 3:复杂嵌套

javascript
console.log('A')

setTimeout(() => {
  console.log('B')
  
  Promise.resolve().then(() => {
    console.log('C')
  })
}, 0)

Promise.resolve().then(() => {
  console.log('D')
  
  setTimeout(() => {
    console.log('E')
  }, 0)
})

console.log('F')

// 输出:A, F, D, B, C, E
// 解析:
// 同步:A, F
// 微任务:D
// 宏任务:B
// 微任务:C
// 宏任务:E

题目 4:async/await 深入

javascript
async function test() {
  console.log('1')
  
  await new Promise(resolve => {
    console.log('2')
    resolve()
  })
  
  console.log('3')
  
  await new Promise(resolve => {
    console.log('4')
    resolve()
  })
  
  console.log('5')
}

test()
console.log('6')

// 输出:1, 2, 6, 3, 4, 5
// 解析:
// 同步:1, 2, 6
// await 将代码分割成多个微任务
// 微任务 1: 3, 4
// 微任务 2: 5

七、实际应用

1. 批量 DOM 更新

javascript
// 使用微任务批量更新 DOM
const updates = []

function scheduleUpdate(element, value) {
  updates.push({ element, value })
  
  // 使用微任务批量处理
  queueMicrotask(() => {
    updates.forEach(({ element, value }) => {
      element.textContent = value
    })
    updates.length = 0
  })
}

// 多次调用只会触发一次 DOM 更新
scheduleUpdate(el1, 'value1')
scheduleUpdate(el2, 'value2')
scheduleUpdate(el3, 'value3')

2. 延迟执行优化

javascript
// 使用 setTimeout 延迟执行
setTimeout(() => {
  heavyComputation()
}, 0)

// 使用微任务(更快)
queueMicrotask(() => {
  heavyComputation()
})

// 或者
Promise.resolve().then(() => {
  heavyComputation()
})

// 性能对比:
// setTimeout: ~4ms 延迟
// queueMicrotask: ~0.1ms 延迟

3. 状态同步

javascript
class Store {
  constructor() {
    this.state = {}
    this.listeners = []
    this.pending = false
  }
  
  setState(newState) {
    this.state = { ...this.state, ...newState }
    
    if (!this.pending) {
      this.pending = true
      
      // 使用微任务批量通知
      queueMicrotask(() => {
        this.listeners.forEach(fn => fn(this.state))
        this.pending = false
      })
    }
  }
  
  subscribe(fn) {
    this.listeners.push(fn)
  }
}

// 多次 setState 只触发一次通知
store.setState({ a: 1 })
store.setState({ b: 2 })
store.setState({ c: 3 })
// 监听器只会被调用一次

八、浏览器与 Node.js 的差异

1. 执行环境对比

┌──────────────────────────────────────────────────────────┐
│          浏览器 vs Node.js 的事件循环                     │
└──────────────────────────────────────────────────────────┘

浏览器 Event Loop:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────────────────────────┐
│         Call Stack              │
└─────────────────────────────────┘

┌─────────────────────────────────┐
│      Microtask Queue            │
│  • Promise callbacks            │
│  • MutationObserver             │
│  • queueMicrotask               │
└─────────────────────────────────┘

┌─────────────────────────────────┐
│      Macrotask Queue            │
│  • setTimeout/setInterval       │
│  • I/O                          │
│  • UI rendering                 │
│  • user input events            │
└─────────────────────────────────┘

Node.js Event Loop:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────────────────────────┐
│         Call Stack              │
└─────────────────────────────────┘

┌─────────────────────────────────┐
│      nextTick Queue             │ ← 特殊队列
│  • process.nextTick()           │
│  (优先级最高,独立于微任务)      │
└─────────────────────────────────┘

┌─────────────────────────────────┐
│      Microtask Queue            │
│  • Promise callbacks            │
│  • queueMicrotask               │
└─────────────────────────────────┘

┌─────────────────────────────────┐
│      Macrotask Queues           │
│  • timers (setTimeout/setInterval)
│  • pending callbacks (I/O)      │
│  • idle, prepare                │
│  • poll (I/O)                   │
│  • check (setImmediate)         │
│  • close callbacks              │
└─────────────────────────────────┘

关键差异:
✓ Node.js 有 process.nextTick(优先级最高)
✓ Node.js 有多个宏任务队列(分阶段执行)
✓ setImmediate 在 poll 阶段后执行
✓ timers 在 timer 阶段执行
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2. Node.js 特有行为

javascript
// Node.js 中:process.nextTick 优先级最高
console.log('start')

process.nextTick(() => {
  console.log('nextTick 1')
  
  process.nextTick(() => {
    console.log('nextTick 2')
  })
})

Promise.resolve().then(() => {
  console.log('promise')
})

console.log('end')

// Node.js 输出:
// start
// end
// nextTick 1
// nextTick 2
// promise

// 浏览器输出(没有 nextTick):
// start
// end
// promise

3. setTimeout vs setImmediate

javascript
// Node.js 中
setTimeout(() => {
  console.log('timeout')
}, 0)

setImmediate(() => {
  console.log('immediate')
})

// 结果不确定,取决于执行时的阶段

// 但在 I/O 回调中
const fs = require('fs')

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

// 总是输出:
// immediate
// timeout

九、面试标准回答

事件循环(Event Loop)是 JavaScript 处理异步任务的机制,它使得单线程的 JavaScript 能够高效地处理大量并发操作。

核心概念包括

  1. 调用栈(Call Stack):执行同步代码,遵循后进先出原则
  2. 任务队列:存储异步任务的回调,分为宏任务队列和微任务队列
  3. 事件循环:不断检查调用栈是否为空,然后处理任务队列中的任务

执行流程是:

  1. 执行所有同步代码(清空调用栈)
  2. 执行所有微任务(Promise.then、queueMicrotask 等)
  3. 渲染 DOM(如需要)
  4. 执行一个宏任务(setTimeout、setInterval 等)
  5. 重复步骤 1-4

宏任务和微任务的区别

  • 宏任务:setTimeout、setInterval、I/O、UI 渲染,每次事件循环只执行一个
  • 微任务:Promise.then、queueMicrotask、MutationObserver,每轮循环清空所有
  • 优先级:微任务 > 宏任务

async/await 的执行

  • async 函数立即执行(同步部分)
  • await 后面的表达式暂停执行
  • await 后的代码作为微任务执行

实际应用中,我会:

  • 使用微任务批量处理 DOM 更新(提高性能)
  • 使用 setTimeout 实现延迟执行
  • 理解 Vue/React 的状态更新机制(基于微任务)

在 Node.js 中,还需要注意 process.nextTick 的特殊性(优先级最高,独立于微任务队列)。


十、记忆口诀

事件循环歌诀:

JS 是单线程,
Event Loop 来处理。
调用栈里放同步,
任务队列存异步!

微任务优先级高,
Promise 和 queueMicro。
宏任务在后面等,
setTimeout 和 I/O!

执行顺序要记牢:
同步代码最先跑,
微任务紧接着,
宏任务最后到!

async/await 特殊:
await 后面是微任务,
暂停恢复自动处理,
异步同步两不误!

十一、推荐资源


十二、总结一句话

  • Event Loop: 调用栈 + 任务队列 = 异步编程核心机制 🔄
  • 宏任务 vs 微任务: 优先级不同 + 执行时机不同 = 正确的执行顺序
  • async/await: 同步写法 + 微任务实现 = 异步编程最佳实践
最近更新