事件循环(Event Loop)详解:JavaScript 异步机制的核心

70 阅读4分钟

事件循环(Event Loop)详解:JavaScript 异步机制的核心

事件循环(Event Loop)可以理解为:JavaScript 用来“排队+按顺序处理任务”的机制
因为 JavaScript 是单线程的(同一时间只能干一件事),所以它需要一套规则来决定:
“现在先执行谁?谁等会儿?谁插队?”


1. 为什么要有事件循环?

如果 JavaScript 只有同步代码还好,但现实中有很多异步操作:

  • 定时器 setTimeout
  • 网络请求(fetch / axios
  • 事件回调(点击、滚动等)
  • Promise

这些异步操作不可能让 JS “卡住等它们完成”,
所以 JS 选择:先继续执行同步代码,异步结果回来后再处理
那么“回来后再处理”靠什么?答案就是——事件循环。


2. 事件循环在干什么?(一句话版)

不停地看:主线程空不空?空了就从任务队列里拿任务来执行。


3. 事件循环的关键结构

✅ 调用栈(Call Stack)

存放同步代码的地方。
执行函数时压栈,执行完弹栈。

✅ 宏任务队列(Task Queue / Macrotask Queue)

存放宏任务,比如:

  • setTimeout / setInterval
  • DOM 事件回调
  • postMessage
  • I/O 操作

✅ 微任务队列(Microtask Queue)

优先级更高,存放微任务,比如:

  • Promise.then / catch / finally
  • queueMicrotask
  • MutationObserver

4. 执行顺序规则

一次事件循环的执行流程:

  1. 先执行 调用栈里的同步代码
  2. 调用栈空了 → 清空所有微任务
  3. 微任务清空后 → 执行 一个宏任务
  4. 重复 1~3

口诀:同步 → 微任务(清空) → 宏任务(一个) → 循环


5. 一个经典示例

console.log(1)

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

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

console.log(4)

执行顺序分析:

  • 同步代码先执行:输出 14
  • 同步执行完毕,清空微任务队列:输出 3
  • 最后执行宏任务队列中的任务:输出 2

所以最终输出是:

1
4
3
2

6. 总结

事件循环是 JavaScript 处理异步的机制。
JS 先执行调用栈里的同步代码,栈空后会优先清空微任务队列(比如 Promise.then),然后执行一个宏任务(比如 setTimeout、事件回调),不断重复这个过程。 好的,接下来给你几个更有挑战性的事件循环面试题,让你在面试中可以轻松应对:


7. 例子 1:异步代码与事件循环

console.log('Start')

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

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

console.log('End')

输出顺序是?

解释:

  • 同步执行:'Start''End'
  • setTimeout 是宏任务,会被放入宏任务队列,等主线程空了才会执行。
  • Promise.resolve().then() 是微任务,微任务队列在当前同步代码执行完后立刻执行。
  • 所以输出顺序是:
    Start
    End
    Promise
    Timeout
    

8. 例子 2:多重微任务和宏任务

console.log('Start')

setTimeout(() => {
  console.log('Timeout 1')

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

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

console.log('End')

输出顺序是?

解释:

  • 首先,执行同步代码:'Start''End'
  • 然后执行第一个微任务:'Promise 1'
  • 接下来,主线程空了,开始执行宏任务:'Timeout 1'
  • 宏任务中的异步代码生成了一个微任务:'Promise 2'
  • 最后,执行 'Promise 2'
  • 所以输出顺序是:
    Start
    End
    Promise 1
    Timeout 1
    Promise 2
    

9. 例子 3:嵌套的微任务和宏任务

console.log('Start')

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

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

console.log('End')

输出顺序是?

解释:

  • 首先执行同步代码:'Start''End'
  • 然后执行第一个微任务:'Promise 1'
  • 接下来,主线程空了,开始执行宏任务:'Timeout 1'
  • 'Timeout 1' 中创建了一个微任务:'Promise 2',并且又创建了一个新的宏任务:'Timeout 2'
  • 执行 'Promise 2' 后,由于事件循环的规则,它会继续执行微任务,所以 'Timeout 2' 会被推迟到当前宏任务执行完后。
  • 所以输出顺序是:
    Start
    End
    Promise 1
    Timeout 1
    Promise 2
    Timeout 2
    

10. 例子 4:多次 setTimeoutPromise

console.log('Start')

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

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

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

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

console.log('End')

输出顺序是?

解释:

  • 执行同步代码:'Start''End'
  • 执行第一个微任务:'Promise 1'
  • 然后执行第一个宏任务:'Timeout 1'
  • 然后执行第二个微任务:'Promise 2'
  • 然后执行第二个宏任务:'Timeout 2'
  • 所以输出顺序是:
    Start
    End
    Promise 1
    Timeout 1
    Promise 2
    Timeout 2
    

总结

通过这些例子,你可以看到事件循环的基本工作原理。理解 同步代码、微任务、宏任务的顺序 是关键。如果理解了这些基本的规则,面对更复杂的异步逻辑时就能迅速推断出执行顺序。