🔥从进程线程聊到JS执行机制,这次彻底搞懂事件循环!(上)

209 阅读8分钟

前言:从“单线程”到“异步”

你是否曾在面试中被问到:“说说 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)管理 setTimeoutsetInterval 等定时任务
异步任务线程(Async Task Thread)处理异步请求(如 fetchXMLHttpRequest
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单线程的设计理念,下一期我们将走进事件循环