深入理解 Event Loop:JavaScript 异步执行的核心机制
本文将从操作系统进程与线程的基本概念出发,结合浏览器多进程架构和 JavaScript 单线程模型,深入剖析 Event Loop 的运行机制,并解释宏任务、微任务之间的关系,以及它们如何影响页面渲染和性能优化。
一、进程与线程
进程(Process)
- 是 CPU 分配资源的最小单位。
- 每个进程拥有独立的内存空间和进程 ID(PID),一个进程崩溃不会影响其他进程。
- 程序运行是以进程为单位进行的。
- 主进程负责管理子进程,实现并发与并行操作。
- 进程之间通信(IPC)开销较大,尤其是无关联进程之间的通信;父子进程之间的通信效率更高。
线程(Thread)
- 是 CPU 调度的最小单位。
- 线程是进程内部的执行单元,多个线程共享同一个进程的资源。
- 在浏览器中,每个进程可以包含多个线程,例如主线程、定时器线程、网络线程等。
二、浏览器的多进程架构
现代浏览器采用 多进程架构 来提高安全性和稳定性:
-
启动时会有一个主进程,负责协调和管理子进程。
-
每个标签页(Tab)对应一个渲染进程,彼此互不干扰。
-
渲染进程中包含主线程(JS 执行线程)、渲染线程、事件触发线程
主线程负责解析 HTML 构建 DOM 树、解析 CSS 构建 CSSOM 树,合并生成渲染树、布局计算(Layout)、图层合并(Paint)、V8 引擎执行 JavaScript。
渲染线程负责将渲染树(Render Tree)转换为屏幕上的像素(栅格化),并处理重绘(Repaint)和回流(Reflow)。
**主线程与渲染线程是互斥的:**同步 JS 执行期间,页面不会重新渲染。JS 执行完毕后,才会触发渲染流程(布局、绘制等)。即使渲染任务已经准备好,也必须等待 JS 主线程空闲后才能执行。
三、Event Loop:JavaScript 异步执行的引擎
由于 JavaScript 是单线程的,为了不让主线程被长时间阻塞,JavaScript 引入了 异步非阻塞模型,其背后的核心机制就是 Event Loop。
执行栈(Call Stack)
- 所有同步代码都在执行栈中按顺序执行。
- 遇到函数调用就将其压入栈顶,函数返回则弹出。
宿主环境(Host Environment)
- 浏览器提供了一些额外的功能,如
setTimeout、setInterval、fetch、addEventListener等。 - 这些功能由不同的线程或系统调用完成(如定时器线程、网络线程)。
- 当这些异步操作完成后,它们会将回调放入相应的任务队列中。
宏任务队列(Macro Task Queue),每次 Event Loop 循环处理一个宏任务。
宏任务包括:setTimeout、setInterval、setImmediate(Node.js 中)、I/O 操作、UI 渲染(某些浏览器中)
微任务队列(Micro Task Queue),微任务优先级高于宏任务,在当前宏任务结束后立即清空所有微任务。
微任务包括:Promise.then, .catch, .finally、queueMicrotask、MutationObserver
四、Event Loop 的执行流程详解
完整的 Event Loop 执行流程如下:
- 宏任务进入执行栈,执行同步任务(进入执行栈)
- 遇到异步任务setTimeout、fetch
- 交给对应的宿主线程处理(如定时器线程),处理完成后,将回调放入对应的任务队列
- 同步任务执行完毕后检查是否有微任务,如果有,依次执行直到微任务队列清空
- 渲染页面
- 执行一个宏任务
- 重复上述流程
五、关于 setTimeout 的一些细节
console.log("Start");
setTimeout(() => {
console.log("Timeout 1");
}, 0);
Promise.resolve().then(() => {
console.log("Promise 1");
});
setTimeout(() => {
console.log("Timeout 2");
}, 0);
console.log("End");
输出结果为:
Start
End
Promise 1
Timeout 1
Timeout 2
分析:
"Start"和"End"是同步任务,直接打印。Promise.then是微任务,进入微任务队列。- 两个
setTimeout是宏任务,进入宏任务队列。 - 同步任务执行完后,先执行微任务(
Promise 1),然后才轮到宏任务。
七、Event Loop 实战技巧与优化建议
-
使用
Promise替代setTimeout(..., 0)来实现异步延迟,因为 Promise 属于微任务,优先级更高,执行更快。// 不推荐 setTimeout(() => { /* ... */ }, 0); // 推荐 Promise.resolve().then(() => { /* ... */ }); -
控制宏任务数量,避免阻塞主线程
避免大量使用
setTimeout或setInterval。可以使用requestIdleCallback(浏览器环境下)来利用空闲时间执行低优先级任务。 -
使用
Web Worker处理耗时任务将计算密集型任务移出主线程,避免阻塞渲染和交互。
八、总结
| 概念 | 描述 |
|---|---|
| 进程 | CPU 分配资源的最小单位,具有独立内存空间 |
| 线程 | CPU 调度的最小单位,多个线程共享进程资源 |
| 主线程 | 执行 JavaScript 同步代码,负责页面渲染与 DOM 操作 |
| Event Loop | 协调宏任务与微任务的执行顺序 |
| 宏任务 | 如 setTimeout、setInterval,每次循环执行一个 |
| 微任务 | 如 Promise.then,优先级更高,同步任务执行完立刻执行 |