很多人学完 Event Loop,面试时还是会被问懵:
- JS 为什么是单线程?
- 浏览器不是多进程多线程吗,怎么配合的?
setTimeout到点了为什么还要等?addEventListener的回调是怎么触发的?- 同步、宏任务、微任务到底怎么排队执行?
这篇文章帮你把 JS 执行机制 从原理到执行顺序一次性理顺,
以后遇到异步顺序题和底层原理面试,直接拿来就能讲!🚀
🧵 一、JS 为什么必须单线程?
先记住:JS 是单线程,执行 JS 的就是主线程。
为啥不能多线程?
- 🔒 安全性:JS 要操作 DOM,如果多个线程同时改 DOM,状态很容易混乱,必须加锁,复杂度瞬间飙升。
- 🧩 简单性:单线程执行,V8 引擎只需要维护一个调用栈,切换上下文简单,性能更高。
所以,JS 从诞生那天起就是单线程,核心就是一个线程跑完所有 JS。
🗂️ 二、浏览器为什么是多进程多线程?
✅ 1)先搞清楚进程 vs 线程
- 进程:资源分配的最小单位(内存、CPU 时间),每个进程独立运行,互不干扰。
- 线程:执行的最小单位,真正干活的是线程,一个进程可以开多个线程。
💡 类比:
进程像是一个办公室,线程是里面干活的员工。
✅ 2)浏览器的多进程架构
为什么要多进程?
- 保证稳定性、隔离性。
- 每个 Tab 对应一个独立的渲染进程(Render Process),互不干扰。一个页面崩了,别的页面不受影响。
经典架构:
- 🏢 浏览器主进程(Browser Process)
负责页面管理、网络资源、插件、进程间通信。 - 🗂️ 渲染进程(Render Process)
每个页面一个,解析 HTML/CSS/JS,构建渲染树,执行 JS,最后渲染到屏幕。
✅ 3)渲染进程也是多线程
很多人不知道,一个渲染进程内部也是多线程的:
| 线程 | 干啥 |
|---|---|
| JS 主线程 | 执行所有 JS 代码(V8)、解析 HTML、构建 DOM 树、执行布局和绘制 |
| GUI 渲染线程 | 负责把渲染树绘制到屏幕 |
| 定时器模块(或线程) | 负责 setTimeout / setInterval 的计时 |
| 网络模块(或线程) | 处理 fetch / XHR 请求的下载和状态变更 |
| 宿主检测模块 | 负责监听用户交互(比如 addEventListener),检测事件是否触发,然后把回调放进队列 |
⚡ 重点:
执行 JS 的永远是主线程,其他模块(或线程)只负责检测条件是否满足,
最后都要把回调丢给主线程去执行。
⚡ 三、JS 和页面渲染为什么是互斥的?
很多人没意识到:
- JS 可能会修改 DOM,而 DOM 会影响渲染树。
- 如果渲染到一半 DOM 被改了,页面就会渲染错乱。
所以浏览器设计了互斥机制:
✅ 当 JS 正在执行时(主线程繁忙),GUI 渲染会挂起,等 JS 执行完毕,
浏览器再根据最新的 DOM + CSS 重新构建渲染树,重新布局和绘制。
这就是为什么大段同步 JS 代码会导致页面卡顿或白屏。
⏲️ 四、为什么 setTimeout 有时不准?
很多人以为 setTimeout 是到点就立刻执行,实际上不是。
- 定时器模块(或宿主线程)只负责计时,到点后把回调放进宏任务队列。
- 如果此时主线程还在执行同步任务或微任务,回调只能继续等。
✅ 归根结底:
定时器准不准,取决于主线程忙不忙,不是定时器线程有多准。
🔄 五、addEventListener 回调是怎么被触发的?
这里特别容易混淆!
✅ addEventListener 本身并不会单独开一个线程去跑回调!
✅ 宿主环境(浏览器底层)会有模块或机制检测用户交互,比如鼠标点击、输入框输入等。
✅ 当事件被触发,浏览器把对应的回调封装成一个宏任务,放进宏任务队列。
✅ 真正执行时机:等主线程执行完同步任务、清空微任务后,轮到宏任务阶段才执行。
所以:
addEventListener没有“独立线程”,监听动作是宿主环境完成的,执行永远靠主线程。
🕰️ 六、宏任务 vs 微任务,到底谁先执行?
Event Loop 的执行顺序永远是:
- 执行一个宏任务(比如
<script>) - 同步任务执行完后,立刻执行所有微任务(
Promise.then、MutationObserver、queueMicrotask) - 微任务队列清空后,再执行下一个宏任务(比如
setTimeout回调)
🚩 小细节:Promise.resolve() 本身不是微任务!
只有
.then()注册的回调才会进入微任务队列。
✅ 经典顺序示例
console.log('脚本开始');
setTimeout(() => {
console.log('宏任务:定时器回调');
}, 0);
Promise.resolve().then(() => {
console.log('微任务:Promise.then');
});
console.log('脚本结束');
输出:
脚本开始
脚本结束
微任务:Promise.then
宏任务:定时器回调
🗂️ 七、宿主模块 + Event Loop 如何配合?
| 场景 | 宿主负责啥 | 回调怎么执行 |
|---|---|---|
setTimeout | 定时器模块计时 | 到点放进宏任务队列,主线程执行 |
fetch / XHR | 网络模块负责下载 | 下载完成后放进宏任务队列 |
addEventListener | 宿主检测事件是否触发 | 事件触发后放进宏任务队列 |
Promise | .then 回调 | 放进微任务队列,优先执行 |
✅ 所有这些执行,最终都离不开主线程排队跑。
🏆 八、面试背诵版核心点
- ✅ JS 是单线程,执行靠主线程,保证对 DOM 操作安全、简单。
- ✅ 浏览器是多进程(主进程 + 渲染进程),每个渲染进程又是多线程(或多模块)。
- ✅ 执行顺序:同步任务 → 微任务 → 宏任务,循环往复。
- ✅
setTimeout不准是因为要等主线程空闲。 - ✅
Promise.resolve()本身不是微任务,.then()才是。 - ✅
addEventListener没有独立线程,监听在宿主,执行靠主线程。
🚀 写在最后
彻底搞懂「JS 单线程 + 浏览器多进程多线程 + Event Loop 执行顺序」,
你就能在面试时顺着执行顺序一口气解释清楚,
从「为什么是单线程」讲到「宿主线程配合」「任务队列调度」到「为什么渲染和 JS 互斥」都能一套带走!
觉得有用?点赞、收藏、转发,让更多人避开异步顺序大坑!咱们一起把前端底层搞透!💪