在前端面试中,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)
执行流程:
- JS 代码交给 定时器线程
- 1 秒后定时器结束
- 回调函数进入 消息队列
- 主线程执行该任务
六、什么是 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 的异步问题都会迎刃而解。