前言:从"单线程"到"异步"
你是否曾在面试中被问到:"说说 JavaScript 的事件循环(Event Loop) ?"
是否曾在实际开发中遇到过这样的问题:为什么 Promise.then
会比 setTimeout
先执行?为什么 setTimeout(fn,0)
并不是"立刻执行"?
甚至,你是否曾经疑惑:JavaScript 是单线程的,那它是如何做到"看起来很聪明",处理各种异步操作的?
要真正理解这些问题,我们需要跳出 JavaScript 本身,把目光投向浏览器。JavaScript 的单线程模型,是它最独特的设计之一,而事件循环,正是这套机制背后的核心驱动力。
但事件循环并不是"孤立运行"的。它背后依赖于浏览器的多进程架构 、多线程协作,以及一套精妙的任务调度机制。JavaScript 主线程负责执行代码,定时器线程负责计时,网络线程负责请求,渲染线程负责页面更新。它们各司其职,又通过事件循环紧密配合。
在这两篇文章中,我们将深入讲解:
- 从 进程与线程的基本概念 讲起,
- 深入 浏览器的多线程架构,
- 揭开 事件循环的执行流程,
- 并结合实际代码剖析 宏任务与微任务的区别 , 最终让你对 JavaScript 的异步执行机制有一个 系统、清晰、可落地 的理解。
无论你是想深入前端底层原理,还是准备迎接下一场技术面试,这篇文章都将为你打下坚实的基础。
进程&线程
进程(Process)
-
定义:进程是程序的一次运行实例,是操作系统进行资源分配和调度的最小单位。
-
特点:
- 每个进程都有自己的 独立内存空间(包括代码段、堆、栈等)。
- 进程之间是 相互隔离 的。
- 一个程序可以有多个进程(如浏览器打开多个标签页)。
-
开销:
- 创建和销毁进程开销大,进程之间的切换也需要花费较多资源。
举个例子:如果我们现在桌面上打开了浏览器 和网易云音乐 ,那么操作系统会为每个程序创建一个进程(Process),浏览器是一个进程(比如 Chrome 的每个标签页可能是一个独立渲染进程),网易云音乐是另一个进程。
这两个进程彼此隔离,互不干扰。即使浏览器崩溃了,网易云音乐也不会因此崩溃。
单进程模型&多进程模型
而我们的应用程序(APP) ,又分为单进程模型 和多进程模型
在单进程模型中,所有功能都在一个进程中执行,例如早期的浏览器(IE6),或者老版本的笔记本,都是单进程,它们的实现比较简单,不需要进行跨进程通信,但是如果一个模块出了错(比如某个插件加载失败),整个进程都会崩溃。
多进程模型 最好的例子就是我们现在用的浏览器了,比如Chrome浏览器,其采用多进程架构,包括:
- 浏览器主进程(Browser Process) :负责管理窗口、标签页、安全策略等。
- 渲染进程(Renderer Process) :每个标签页可能是一个独立的渲染进程,负责解析 HTML、执行 JS、渲染页面。
- GPU 进程(GPU Process) :负责图形渲染。
- 网络进程(Network Process) :处理所有网络请求。
- 插件进程(Plugin Process) :运行第三方插件(如 Flash)。
多进程模型有很多优点,比如它的隔离性比较好 ,一个标签页崩溃不会影响整个浏览器的运行,它的稳定性比较强,关键功能都是独立进程处理,它还能用多核CPU并行处理任务。
至于它的缺点,就是每个进程都有独立的内存,占用空间较大,再者就是进程间通讯比较复杂。
线程(Thread)
-
定义:线程是 CPU 调度的最小单位,一个进程可以包含多个线程。
-
特点:
- 同一进程下的线程共享该进程的资源(如内存、变量等)。
- 线程之间的切换比进程快得多。
- 线程之间容易通信和共享数据。
-
注意:
- 多线程并发执行时,要注意线程安全问题(比如资源竞争)。
以浏览器为例,它内部可能有:
- JS 主线程:执行 JavaScript 代码
- 渲染线程:负责页面绘制
- 网络线程:处理 HTTP 请求
- 定时器线程:管理 setTimeout
- 合成线程:负责页面合成和渲染优化
而网易云音乐也可能有:
- 音频播放线程:播放音乐
- UI 线程:更新界面
- 网络线程:加载歌词、封面、流媒体数据
这些线程都在各自的进程中运行,协同工作。
线程之间的协作:并发 vs 并行
你的 CPU 可能只有一个或多个物理核心,但你同时在听歌、看网页、甚至在浏览器中还有多个标签页在运行。
这是如何做到的?
这就涉及到调度器(Scheduler) 的工作:
- 并发(Concurrency) :多个任务交替执行(不是真正同时)
- 并行(Parallelism) :多个任务同时执行(需要多个 CPU 核心)
在只有一个 CPU 核心的场景下,操作系统会通过时间片轮转的方式,让多个线程轮流执行,从而实现"并发"。
比如:
- 浏览器的 JS 主线程执行一段代码
- 时间片到,操作系统调度器切换到网易云音乐的音频线程
- 之后又切换回浏览器的网络线程,继续加载资源
你感觉它们在"同时运行",其实只是切换得非常快(通常每几毫秒切换一次)。
浏览器的多线程架构
浏览器是多进程的,多线程的 ,就拿浏览器的渲染进程来说,我们每打开一个页面就有一个新的渲染进程产生,渲染进程中主要有下面几个线程:
线程名称 | 功能说明 |
---|---|
主线程(Main/UI Thread) | 执行 JavaScript、解析 HTML/CSS、处理用户交互(点击、滚动等) |
V8 引擎线程(JS Engine Thread) | 执行 JavaScript 代码,管理 JS 的堆栈和垃圾回收 |
渲染线程(Rendering Thread) | 负责将 DOM + CSSOM 合成 Render Tree、布局(Layout)、绘制(Paint) |
合成线程(Compositor Thread) | 合成页面中的各个图层,准备最终显示画面 |
光栅化线程(Raster Thread) | 将页面内容转换为像素,准备显示 |
IO 线程(IO Thread) | 处理与其它进程(如网络进程)的通信,加载资源 |
定时器线程(Timer Thread) | 管理 setTimeout 和 setInterval 等定时任务 |
异步任务线程(Async Task Thread) | 处理异步请求(如 fetch 、XMLHttpRequest ) |
Web Worker 线程(Worker Threads) | 在后台执行 JavaScript,不会阻塞主线程 |
线程的配合(打开新网页为例)
那么线程之间是如何配合的呢?我们就以新开一个网页为例子吧!
当你打开一个网页时,浏览器的线程们是这样协作的:
IO 线程 从网络线程获取 HTML 数据
主线程解析 HTML,构建 DOM 树
遇到 <script>
标签,暂停解析,交给 V8 引擎线程执行 JS
执行完 JS 后,继续解析 HTML/CSS,生成 CSSOM
构建 Render Tree,进行 Layout(布局)
交给 光栅化线程生成像素图像
合成线程将各图层组合成最终画面并显示
用户交互(如点击按钮)触发事件处理,回到主线程执行
看到这里大家应该对于进程线程有了基本的理解了,从浏览器的多进程多线程设计,我们透过门的一个小缝,瞥见了JavaScript
这门语言的设计,开始理解了它为什么是一门单线程语言 ,下面我们将扒了他的朝服来看看究竟怎么个事
JavaScript
?单线程?
在 JavaScript
引擎(如 V8、SpiderMonkey)中,同一时间只能执行一段代码 ,不能并行执行多个 JavaScript
任务。
JavaScript 诞生于 1995 年,最初是为了在网页中添加一些简单的交互(如表单验证、动画等)。当时的设计目标是:
- 简单易用:开发者不需要处理复杂的多线程问题(如死锁、竞态条件等)
- 快速实现:浏览器实现更简单,运行效率更高
- 避免资源争用:如果多个线程同时操作 DOM,会导致状态不一致
如果 JavaScript
是一个多线程语言的话,它将面对一些问题:
假设有两个线程同时操作一个 DOM 元素:
- 线程 A:修改元素颜色为红色
- 线程 B:修改元素颜色为蓝色
这时候浏览器不知道该优先执行哪个操作,会引发同步问题(race condition)。
所以为了避免这些复杂性,它的设计者把它设计成了一门单线程语言,毕竟这门语言只用了仅仅一周的时间就设计出来 了,最开始做的也不是很复杂的工作,所以就以简为优。
而单线程有一个不好的地方就是不能实现异步 ,但是JavaScript
借助浏览器的其他线程和node.js
的某些能力是可以实现异步编程的,其和事件循环 相结合,做到了异步非阻塞编程。
而事件循环就是我们下一期需要讲的了......
总结
这一期我们学习了进程和线程的有关概念,知道了进程包含多个线程,而一个APP可以包含多个进程,这些进程与线程相互合作配合,最终共同达到我们想要计算机实现的效果。随后我们介绍了浏览器的多线程架构与JS单线程的设计理念,下一期我们将走进事件循环!