一文彻底理解 JavaScript 事件循环(Event Loop)与消息队列

4 阅读5分钟

在前端面试中,Event Loop(事件循环) 几乎是必考题。但很多人只记住了“宏任务、微任务”,却没有真正理解浏览器是如何调度任务的。

其实想搞懂 Event Loop,需要先理解两个核心概念:

  • 消息队列(Message Queue)
  • 事件循环(Event Loop)

简单来说:

消息队列负责存任务,Event Loop 负责执行任务。

这篇文章会从 浏览器运行机制 → 单线程模型 → 消息队列 → 事件循环 → 宏任务与微任务 一步一步讲清楚。


一、浏览器的运行机制

当我们打开一个网页时,浏览器会创建一个 渲染进程(Renderer Process)

在这个进程中,有一个非常核心的线程:

主线程(Main Thread)

主线程负责很多重要工作,例如:

  • 解析 HTML,生成 DOM Tree
  • 解析 CSS,生成 CSSOM Tree
  • DOM + CSSOM 合并生成 Render Tree
  • 计算布局(Layout)
  • 图层合并(Layer)
  • 页面绘制(Paint)
  • 执行 JavaScript

其中:

  • V8 引擎:负责执行 JavaScript
  • 渲染引擎:负责页面渲染

这两个都运行在 同一个主线程上

也就是说:

JavaScript 执行和页面渲染共享同一个线程。


二、为什么 JavaScript 是单线程?

JavaScript 从设计之初就是 单线程语言

原因是:

JavaScript 需要频繁操作 DOM。

如果 JS 是多线程,就可能发生:

线程 A:

删除某个 DOM

线程 B:

修改这个 DOM

如果两个线程同时操作 DOM,就会导致:

页面状态不可预测。

因此浏览器选择:

JavaScript 使用单线程模型。

优点是:

  • 逻辑简单
  • 避免 DOM 操作冲突

但同时也带来一个问题:

阻塞(Blocking)


三、单线程带来的问题

因为 JavaScript 是单线程的,所以同一时间只能执行一个任务。

例如下面这个伪代码:

int GetInput(){
    int input_number = 0;
    cout<<"请输入一个数:";
    cin>>input_number; // 主线程阻塞
    return input_number;
}

void MainThread(){
    for(;;){
        int first_num = GetInput();
        int second_num = GetInput();
        result_num = first_num + second_num;
        print("最终计算的值为:%d",result_num);
    }
}

当程序执行到:

cin >> input_number

主线程会一直等待用户输入。

在等待期间:

  • 程序无法执行其他任务
  • 页面无法更新
  • 用户操作没有响应

如果浏览器也是这种模式,那么:

  • 请求接口时页面会卡住
  • 点击按钮不会响应
  • 动画会停止

显然这种体验是不可接受的。


四、浏览器的解决方案:消息队列

为了解决单线程阻塞问题,浏览器引入了:

消息机制(Message Mechanism)

核心就是:

消息队列(Message Queue)

简单来说:

消息队列是一个存放待执行任务的队列。

浏览器中有很多任务来源,例如:

  • 用户点击事件
  • 网络请求返回
  • 定时器触发
  • Promise 回调
  • DOM 变化

这些任务不会立即执行,而是会被放入 消息队列

例如:

消息队列

任务A
任务B
任务C

队列遵循:

FIFO(先进先出)

也就是说:

谁先进入队列,谁先执行。


五、任务是如何进入消息队列的?

浏览器内部其实有多个线程,例如:

线程作用
渲染主线程执行 JS、页面渲染
网络线程处理 HTTP 请求
定时器线程setTimeout
IO 线程文件读取

当这些线程完成任务时,会向主线程发送 消息(Message)

例如:

setTimeout

setTimeout(() => {
  console.log("timeout")
}, 1000)

执行流程:

  1. JS 代码交给 定时器线程
  2. 1 秒后定时器结束
  3. 回调函数进入 消息队列
  4. 主线程执行该任务

六、什么是 Event Loop?

有了消息队列之后,还需要一个机制来执行这些任务。

这个机制就是:

Event Loop(事件循环)

Event Loop 的核心作用是:

不断从消息队列中取出任务并执行。

逻辑类似:

while(true){
   从消息队列取任务
   执行任务
}

只要浏览器页面存在,这个循环就会一直运行。


七、宏任务与微任务

在 Event Loop 中,任务分为两种:

  • 宏任务(Macro Task)
  • 微任务(Micro Task)

宏任务

常见宏任务:

script
setTimeout
setInterval
I/O
UI事件
postMessage

例如:

setTimeout(() => {
  console.log("timeout")
})

会进入 宏任务队列


微任务

微任务优先级更高。

常见微任务:

Promise.then
MutationObserver
queueMicrotask
async/await

例如:

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

会进入 微任务队列


八、Event Loop 执行流程

Event Loop 的执行顺序非常重要:

1 执行一个宏任务

浏览器启动后,第一个宏任务通常是:

script

2 清空所有微任务

当前宏任务执行完后:

会一次性执行完所有微任务。


3 浏览器渲染

如果有 DOM 更新,浏览器会进行页面渲染。


4 执行下一个宏任务

整个流程循环执行:

宏任务
↓
清空所有微任务
↓
浏览器渲染
↓
下一个宏任务

九、经典面试题

来看一个经典例子:

console.log(1)

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

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

console.log(4)

执行流程:

执行 script

输出:

1

注册任务:

setTimeout → 宏任务队列
Promise.then → 微任务队列

继续执行:

4

执行微任务

执行:

3

执行宏任务

执行:

2

最终结果:

1
4
3
2

十、总结

理解 Event Loop 可以总结为五点:

1 JavaScript 是单线程

JS 执行和页面渲染共享同一个线程。


2 浏览器使用消息队列存任务

所有异步任务都会进入消息队列排队执行。


3 Event Loop 负责执行任务

不断从消息队列中取任务执行。


4 任务分为两种

宏任务:

script
setTimeout
setInterval
I/O
UI事件

微任务:

Promise.then
async/await
MutationObserver

5 执行顺序

执行宏任务
↓
清空所有微任务
↓
浏览器渲染
↓
执行下一个宏任务

结语

Event Loop 是理解 JavaScript 异步机制的核心基础。

很多技术都建立在它之上,例如:

  • Promise
  • async / await
  • React 更新机制
  • Node.js 运行机制
  • 浏览器性能优化

如果准备前端面试,Event Loop 不仅要记住概念,更要理解执行流程。

只有真正理解了 消息队列 + 事件循环,很多 JavaScript 的异步问题都会迎刃而解。