作者:一只努力在面试里不结巴的前端小白
适用人群:前端实习、面试、自我夸夸、装 X
前言
要问前端最容易被面试官随口一问问懵的八股文是啥?
那肯定是:
- 浏览器到底是多进程还是多线程?
- JS 为什么是单线程?
- Event Loop 是怎么调度的?
- setTimeout 为什么不准?
- JS 和页面渲染为啥互斥?
别急,看完这篇,从多进程到微任务,一条龙全部安排明白,把面试官问得沉默是我的 KPI。
CPU 轮循:计算机的时间片马戏团
先别急着扔词儿,咱从操作系统讲起:
CPU 其实就是个特别讲秩序的大管家——谁都想上去跑,但 CPU 核心(core)一个时刻只能跑一个任务,咋办?
用时间片轮转调度!
- 操作系统把 CPU 时间切成无数个小时间片,像切蛋糕一样分给不同的进程和线程轮流跑,切换得贼快,你肉眼看着像在同时跑,其实是排队+轮流干活。
进程:程序界的独立小隔间
进程(Process) ,就是一个正在操作系统里跑的程序单元。
特点:
- 分配资源(内存、文件句柄)的最小单元
- 拥有独立的 PID(进程 ID)
- 有自己的私有内存空间
- 创建和销毁比线程开销大
就像公司办公室里的独立工位,一个人一个座位,互不打扰。
线程:CPU 调度的最小单元
线程(Thread) ,是 CPU 真正调度时干活的家伙。
- 进程是办公室,线程是坐在办公室里敲键盘的员工。
- 一个进程里可以有多个线程——多线程并发干活更高效。
- 同一个进程里的线程共享内存,沟通方便,切换也快。
主进程 & 子进程:有爹娘就是香
- 一个程序(主进程)可以拉起子进程(比如
Node.js的child_process)。 - 父子进程间能用管道、共享内存传话,效率比两个陌生进程直接沟通高多了。
Chrome:多进程浏览器,稳到离谱
现代浏览器(以 Chrome 为代表)就是把「多进程」用到了极致。
为什么多进程?
- 安全:每个标签页/iframe/插件都单独开一个渲染进程,互相隔离,防止恶意脚本搞大新闻。
- 稳定:一个页面崩了只炸自己的小隔间,不会带崩整个浏览器。
- 多核利用:多进程 + 多线程,把多核 CPU 用爽。
Chrome 都有哪些进程?
| 进程 | 作用 |
|---|---|
| 浏览器主进程 | 管整个浏览器 UI、标签管理、进程调度。 |
| 渲染进程 | 每个页面一个,干 HTML/CSS/JS 渲染的活儿。 |
| GPU 进程 | 干图形加速、合成、绘制(显卡亲自下场)。 |
| 插件进程 | 第三方插件单开(没落的 Flash 泪目)。 |
渲染进程里:多线程齐上阵
一个渲染进程里,线程们各司其职,互相搭配:
| 线程 | 干啥的 |
|---|---|
| 渲染主线程(主角) | 执行 JS、解析 HTML/CSS、构建 DOM 树、CSSOM、Render Tree,布局,安排绘制,统统跑在这。 |
| 定时器线程 | 专门负责 setTimeout、setInterval 计时,到点就把 callback 丢到宏任务队列。 |
| 网络线程 | 管 fetch、XHR,请求发出去、下载回来都走它,完了把回调丢到队列里。 |
| 独立绘制线程 | 真正把合成好的图层丢给 GPU,画到屏幕上。 |
JS 单线程:为啥单线程不香吗?
为啥 JS 单线程?难道不能多线程吗?
不能。或者说——不敢。
因为 JS 可以随时读写 DOM,如果多线程同时读写:
- 线程 A 改一半,线程 B 改另一半,那 DOM 树炸了,谁的版本对?
所以 W3C 一拍脑门:干脆单线程,谁都别抢。
安全!省心!可控!
代价就是单线程只能一个一个跑,卡主线程就卡 UI。
setTimeout 背后的定时器线程 & 不准的真相
很多人以为:
setTimeout(() => console.log('Hi'), 0);
就会立刻执行?别傻了!
真相:
setTimeout(fn, delay)只能保证「至少延迟 delay 毫秒」。
它的流程:
- JS 把定时器扔给浏览器的定时器模块(底层线程)。
- 到点后,这个模块把 callback 丢到宏任务队列。
- 事件循环轮到它了,主线程空闲了,才执行。
为什么会不准?
- JS 是单线程,如果你有同步大任务卡住主线程,那
setTimeout就得排队等。 - 浏览器有最小时间片限制(比如 Node.js 有个最小 4ms)。
- 结果:只能保证 最短等待,不能保证准时。
经典案例:
setTimeout(() => console.log('A: 0ms'), 0);
setTimeout(() => console.log('B: 10ms'), 10);
while (Date.now() < Date.now() + 100) {} // 阻塞 100ms
结果:
A: 0ms
B: 10ms
因为两者都到期了,但主线程 100ms 都被 while 占了,谁先注册谁先执行。即如果两个定时器顺序反一下,则先输入10ms,再输出0ms的。
addEventListener 背后没有独立线程?
对!addEventListener 的本质:
- 浏览器底层(输入线程)感知点击/输入事件。
- 事件触发后,把回调放宏任务队列。
- 真正执行还是跑在 JS 主线程。
所以:
addEventListener没有像定时器那样的专属线程。- 主线程要是被阻塞(如死循环),你点破鼠标也没用,事件永远排不上队执行。
fetch/xhr 的「专属线程」咋理解?
- 网络请求是浏览器/Node 的网络模块单独跑的。
- 下载和 TCP 连接不会占 JS 主线程。
- 下载完才把回调扔到队列里。
- 真正处理返回结果(解析 JSON、改 DOM)还是要回到主线程。
Event Loop:宏任务 & 微任务
| 队列 | 代表 |
|---|---|
| 宏任务 | setTimeout、setInterval、fetch 回调、DOM 事件 |
| 微任务 | Promise.then、queueMicrotask、MutationObserver |
顺序:
- 执行同步任务
- 执行完立刻清空微任务队列
- 执行一个宏任务
- 有空就渲染
- 重复 1
JS 和渲染:为什么互斥?
来了来了,今天最核心的一个点!
JavaScript 执行时,页面渲染会被挂起,互斥运行。
为啥?
-
JS 可以改 DOM、改 CSS。
-
渲染也要用 DOM、CSS。
-
如果两边同时跑:一边算一半 DOM 树,JS 改了一半,渲染挂了,页面就花了。
-
所以浏览器的原则是:
- 同一时刻,要么跑 JS,要么跑渲染。
- 保证一致性,别把页面搞崩。
这就是为什么:
- JS 执行特别久(死循环、大循环),页面就会卡住,不会重绘。
- 高效的做法是:把耗时任务拆小块,让渲染线程有机会插空渲染,比如用
requestAnimationFrame分帧更新。
结尾:这套面试必杀句
✅ 浏览器是多进程的,Chrome 每个 tab/iframe 是独立渲染进程,稳定安全。
✅ 渲染进程是多线程的,V8 执行 JS 的主线程、定时器线程、网络线程、绘制线程各司其职。
✅ JS 是单线程的,保证了 DOM 操作的原子性,不用加锁,简单安全。
✅ Event Loop 负责把异步回调从宏任务/微任务队列调度给主线程执行。
✅ JS 和渲染互斥,防止状态错乱,所以大任务要拆分,别卡主线程。
背完这套,面试官听了默默点头:
这小子,稳了!
如果觉得有用,点个赞,码字不易,让更多小伙伴少踩坑!
我是一个在面试路上磕磕绊绊但越磕越勇的前端小白,后面我会持续分享 JS 原理、浏览器底层、性能优化、面试经验,别走开,我们下篇掘金见!