面试必问!JS 单线程、浏览器多进程、Event Loop 执行顺序一篇全懂

108 阅读5分钟

很多人学完 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 的执行顺序永远是:

  1. 执行一个宏任务(比如 <script>
  2. 同步任务执行完后,立刻执行所有微任务(Promise.thenMutationObserverqueueMicrotask
  3. 微任务队列清空后,再执行下一个宏任务(比如 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 互斥」都能一套带走!


觉得有用?点赞、收藏、转发,让更多人避开异步顺序大坑!咱们一起把前端底层搞透!💪