本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
🌻 前言
作为一个前端工程师,浏览器是我们密不可分的朋友,想要深耕前端,就必须和浏览器"交心"💞。
为什么你觉得偶尔看浏览器的工作原理,但总是忘呢😵💫,因为你没有形成一个完整的知识网络,你的记忆是碎片化的。正如人的神经网络,只有当你的记忆相互依赖,相互链接,才能形成长期稳定的记忆。
所以本文我将用一条知识线将浏览器工作原理的知识串联起来,因为本文的目的是为了帮助大家建立浏览器基础的思维树,所以很多细节点不做过多阐述,先有了树,后面你在上面伸展枝叶就会发现清晰明了很多。欢迎点赞支持或评论指正。
🪴一、了解进程、线程
进程:计算机正在运行的一个程序的示例。每个进程都有自己的内存空间、系统资源(如文件描述符、I/O 等),以及一个或多个线程(执行指令的基本单位);
线程:线程是进程中的执行单元,是操作系统调度的最小单位。在一个进程中可以有多个线程,它们共享进程的资源,但拥有各自的执行流。线程是进程的一部分,同一个进程中的多个线程可以并发执行,共享进程的地址空间和系统资源。
通俗的说,进程是一座正在运作的工厂,那么线程就是工厂里面的生产线,生产线包含在工厂中,是工厂中的实际运作单位。
你需要了解:
-
进程具有独立性,所有进程互相隔离,独立运行。一个进程崩溃不会影响其他进程。进程与进程之间的通信,是通过
进程间通信(Inter-Process Communication,简称IPC)
来进行数据交换和协作的。 -
一个进程包含多个线程,每个线程
并行
执行不同的任务。其中一个线程崩溃了,那么整个进程也就崩溃了。线程之间可以相互通信。
并行和并发
🚨关于并行和并发的概念一定要清晰,因为很多问题的根源或者性能优化方案都是依靠并行或并发的。
并发是指多个任务在同一时间段内交替执行,从外部看似乎是同时执行的
并行是指多个任务同时执行,需要多个处理器或多个核心的CPU来支持
关于进程通信(IPC)
进程间通信方式有以下七种:
- 管道(Pipe): 管道是一种半双工的通信方式,它在父进程和子进程之间创建一个共享的文件描述符,通过文件描述符进行通信。管道可以分为匿名管道和命名管道。
- 消息队列(Message Queue): 消息队列是一种进程间通信的机制,进程通过发送消息到队列,其他进程可以从队列中接收消息。消息队列提供了一种异步的通信方式。
- 共享内存(Shared Memory): 共享内存允许多个进程访问同一块物理内存区域,进程可以直接读写共享内存中的数据。共享内存是一种高效的 IPC 方式,但需要额外的同步机制来确保数据的一致性。
- 信号量(Semaphore): 信号量是一种用于进程同步的机制,它可以用来控制对共享资源的访问。信号量可以用于解决临界区问题,确保多个进程按照一定顺序访问共享资源。
- 套接字(Socket): 套接字是一种网络编程中常用的通信方式,它可以在同一台主机上的不同进程间进行通信,也可以在网络上的不同主机上的进程间进行通信。
- 文件映射(File Mapping): 文件映射允许多个进程共享同一文件的内存映射区域,可以通过对这个内存区域的读写来进行进程间通信。
- 信号(Signal): 信号是一种软件中断,用于通知进程发生了某个事件。进程可以通过注册信号处理函数来捕获和处理信号。
根据交换信息量的多少和效率的高低,分为低级通信和高级通信。以上共享内存、管道、消息队列、套接字属于高级通信。
这里简单说下Chrome的 IPC 通信机制。它的渲染器进程通信由 Mojo
实现(以前是使用Chromium IPC),Mojo 是源于 Chrome 的 IPC 跨平台框架,它诞生于 chromium ,用来实现 chromium 进程内/进程间的通信。目前,它也被用于 ChromeOS。
🧨 想了解 Mojo的,可以看下 Mojo 设计者写的文档:Mojo介绍
线程安全
上面我提到了,线程会共享进程的地址空间和系统资源。所以这些拥有共享数据的多条线程处理并发
式任务时,由于多个线程操作同一资源,操作时间并未错开,而使得内存中的操作重复,从而数据错乱,造成线程安全问题。具体线程安全的原因可以总结为以下5点:
- 抢占式执行:多线程的调度是随机的,万恶之源;
- 多线程修改同一个变量:这个很好理解,就是多个线程同时修改一个变量,就会出现问题,这里就不举例了;
- 操作是原子的:简单来说就是每个线程的指令都是独立的,所以修改同一变量时才会出问题,对此问题,我们可以把相同操作的所有指令封装到一起;
- 指令重排序:编译器可能会改变两个操作的先后顺序,处理器也可能不会按照目标代码的顺序执行。内核这样的操作其实是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能,但是同时就会可能影响代码执行的正确性,导致线程安全问题;
- 内存可见性问题:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程应该能够立即看得到修改后的值,但是在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的,所以无法立即同步数据。
总结来讲,就是线程并发或者内核优化机制导致的线程安全问题。那怎么解决线程安全问题呢🤔?一般有以下几种思路:
- 锁机制、互斥量、信号量: 这些都是同步控制的机制,通过对关键区域的加锁,确保在同一时刻只有一个线程能够访问共享资源。加锁是非常常见的解决线程安全的手段;
- 线程安全的数据结构: 使用线程安全的数据结构可以减少对共享数据的直接访问,从而减小同步的压力。
- Web Workers: 在一些需要并行计算的场景下,使用 Web Workers 可以将任务分发到独立的线程中执行,减少对主线程的竞争。
- 事件驱动: 使用事件驱动的编程模型,通过发布和订阅事件来进行通信,可以降低对共享状态的依赖。
- Atomic操作: 使用原子操作,确保某些操作在执行时是不可中断的,避免了在多线程环境下的竞争问题。
🪴二、浏览器有哪些进程?
现在的浏览器基本都是采用多进程架构设计
,即一个标签页(也就是一个网页)会启用多个进程,一般至少有如下四个进程:
- 浏览器进程:整个浏览器的主进程,负责协调、控制其他进程;
- 渲染进程(也可以叫内核进程):负责渲染页面内容的进程;
- 网络进程:负责处理网络请求,每个标签页都共享同一个网络进程,以减少资源占用。
- GPU进程:负责处理浏览器中与图形相关的任务,,例如加速页面绘制、处理 CSS 动画、执行 WebGL 操作等。GPU 进程与渲染进程分离,以提高性能。
除此以外,还可能开辟插件进程,如果页面使用了插件(例如 Flash、Java 等),则会有对应的插件进程。
渲染进程(也叫内核进程,因为浏览器内核、js引擎就是在渲染进程中工作的)和网络进程是我们前端程序员最需要关注的两个进程,下面我们就分别说说这两个进程究竟干了些什么?
🪴三、渲染进程有哪些线程?
五大类线程
渲染进程共有以下5类线程:
- GUI渲染线程:负责渲染网页,具体见下一章,当页面触发重绘、回流时该线程也会执行。
注意:GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。这也就是为什么js文件会阻塞页面加载,一般最好放在html底部引入的原因。
-
JS引擎线程:该线程是使用js引擎处理Javascript脚本程序,解析Javascript脚本,运行代码;
-
事件触发线程:负责处理用户输入和触发相应的事件(例如,点击按钮时,事件触发线程会负责处理这个点击事件)。它管理一个事件队列,当对应的事件被触发时,事件触发线程会把该事件添加到事件队列的队尾,等待JS引擎的处理;
-
定时器触发线程:负责处理通过
setTimeout
和setInterval
等方法设置的定时器,触发相应的回调函数。 -
异步HTTP请求线程:XMLHttpRequest连接后,浏览器会新开一个线程请求,检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待JS引擎空闲后执行;
为什么事件触发、定时器触发、异步异步HTTP请求都会有各自的线程处理呢?因为JS引擎是单线程的,这些异步任务会阻塞js的执行。所以要单独开启几个线程和主线程并行 执行。
这些处理异步操作的线程会把所有异步任务推到一个任务队列里,等待 JS 引擎空闲时,再把他们添加到可执行栈中,开始执行(这里就又延伸到 Event Loop 的机制了)
渲染进程中的异步HTTP请求线程和网络进程有何区别?
看到这里有同学就要问了,渲染进程中的异步HTTP请求线程负责处理异步 HTTP 请求,那浏览器的另一个进程-网络进程是干嘛呢?两个有什么区别?
渲染进程中的异步 HTTP 请求线程 是专门用来处理 JavaScript 层面的异步网络请求的,例如使用XMLHttpRequest
对象或 Fetch API。而网络进程就比较全面了,它负责处理所有的网络操作,包括页面导航、处理主页面的请求、子页面的请求、资源加载等。
另外他们两个是协同工作的,例如 JS 发起的异步网络请求,要经历:DNS 解析:-> 建立连接 -> 发送请求 -> 接收响应
当HTTP 异步请求线程处理 JS 代码发起的请求时,DNS 解析和建立连接通常在网络进程中执行,HTTP 异步请求线程则负责发送请求和接收响应。
🪴四、事件循环机制Event Loop
上面我们说了事件循环机制基于 JS 线程是单线程的。虽然渲染进程有很多线程,但是JavaScript执行是在一个单一的线程中进行的。
以下是事件循环的过程:
- 主线程的任务是不断地执行执行栈中的代码。
- 当执行栈为空时,会检查任务队列中是否有待执行的任务。
- 如果有,将任务队列中的回调函数添加到执行栈,继续执行。
从上面渲染引擎的几大线程角度来说,JS事件循环机制可以这么理解:
当 JS 引擎从上至下按顺序执行代码时,遇到异步任务,会交由其他几个负责异步任务的线程去执行(事件触发线程、定时器线程、异步Http请求线程)并将异步回调推到任务队列里。
想要快速理解事件循环机制?了解以下这几点就明白了,不明白你提刀来砍我🔪🤣👌
-
任务队列有两组,一个是宏任务队列,一个是微任务队列,定时器线程会把定时器回调推到宏任务队列,其他异步任务都会被推到微任务队列;
-
执行时是执行一个宏任务,然后执行这个宏任务里产生的所有微任务,然后再执行一个宏任务,再执行这个宏任务里产生的所有微任务,一直循环下去;
-
其实你可以这么理解,把定时器的回调内容不要当作异步代码,只是被延迟执行的同步代码。上一点就可以理解成执行完所有同步任务,执行所有产生的异步任务,然后再执行所有同步任务,再执行产生的所有异步任务;
-
最后超简单的理解,你可以这么想,忽略宏任务里的setImmediate(node.js的函数,浏览器中不存在)、I/O 操作等,在浏览器中,宏任务其实只需要关注定时器、延时器。所以可以这么想:JS 从上向下执行,同步代码执行完后,执行除了定时器以外的所有异步任务,然后执行一个定时器,再执行所有除了定时器以外的所有异步任务,一直往复下去...
🪴五、网络进程有哪些线程
网络进程里我们需要知道以下线程:
- DNS 解析线程(DNS Thread): 负责进行 DNS 解析,将域名解析为 IP 地址。
- SSL 线程(SSL Thread): 处理 SSL/TLS 相关的加密和安全通信。
- 网络请求线程(Network Thread): 负责处理主要的网络请求和响应,包括接收和发送数据。
- IPC 线程(Inter-Process Communication Thread): 负责进程间通信。
网络进程的主要工作包括:
- 发起和处理网络请求,包括 HTTP 请求、WebSocket 等。
- 执行 DNS 解析,将域名解析为 IP 地址。
- 建立和管理网络连接。
- 处理安全通信,包括 SSL/TLS 加密。
- 处理缓存,包括 HTTP 缓存的读取和写入。
- 处理文件的读取和写入。
- 与其他进程(如浏览器进程)进行通信。
🪴六、详解渲染进程|浏览器引擎工作原理
本章主要讲解浏览器的两个引擎:渲染引擎、js引擎的工作原理。包括渲染引擎的渲染过程、js引擎的工作原理、js引擎的垃圾回收机制、内存泄漏问题定位等
书接下回,点个关注敬请期待(更新后会在此处贴上链接)
🪴七、详解网络进程|Http协议
本章主要讲解与网络进程息息相关的网络通信协议相关的知识,例如TCP/IP协议、TCP建立连接的过程、https加密机制、http首部字段等知识
书接下回,点个关注敬请期待(更新后会在此处贴上链接)
🪴八、网页加载超详细全流程
从 URL 输入到页面展现到底发生了什么?这是一个老生常谈的问题。我将会从网络通信协议、进程协作、浏览器引擎工作流程等全方位讲解这个过程。帮你真正理解网页的加载过程,而不是浮于表面。
书接下回,点个关注敬请期待(更新后会在此处贴上链接)
🎁 说在最后
因为篇幅问题,本文先列出来整体的思维网络,后面详细的一些讲解单独抽离,作为系列文章以后更新,感兴趣的可以点个关注,不要错过~🥳🥳🥳
先看后赞,养成习惯👍
收藏吃灰,不如学会🍗
点个关注,不要迷路🪤