浏览器原理篇
一、浏览器缓存机制
浏览器缓存机制是提升网页加载速度、减少服务器负载和网络带宽消耗的重要技术。
浏览器缓存行为
- 浏览器每次发起请求,都会先在浏览器缓存中查找该请求结果及缓存标识
强缓存(Strong Cache)
特点:可以设置两种HTTP Header实现: Expries和Cache-control。不向服务器发送请求,直接使用本地缓存资源。
Expries(HTTP/1.0)
Expries的值是一个绝对时间(GMT格式),在此时间前会使用缓存,例如:Expires: Wed, 21 Oct 2025 07:28:00 GMT;缺点是Expries受限于本地时间,如果客户端和服务器时间不一致,会判断缓存错误。
Cache-Control (HTTP/1.1 优先级更高)
常用指令有:
max-age=3600: 资源的最大存活时间(单位:秒)资源在3600秒内有效,期间直接使用缓存no-cache: 跳过强缓存,直接进入协商缓存no-store:完全不使用任何缓存,每次都重新请求public:表示该资源可以被任何节点缓存(包括客户端和代理服务器)private:表示该资源只能被客户端缓存,代理服务器不能缓存
当Cache-Control和Expries同时存在时Cache-Control的优先级更高。
强缓存流程:
浏览器发起请求时,会先检查本地是否有该资源的缓存,并计算是否在Cache-Control的max-age或者Expries设定的有效期内。
如果有效: 直接读取磁盘/内存缓存,返回状态码200(from disk cache)或200(from memory cache)
如果失效: 进入下一阶段-协商缓存
协商缓存
如果首次缓存没有Cache-Control和Expries;或者Cache-Control的属性设置为no-cache时,又或者缓存过期,就需要发起请求验证资源是否有更新。向服务器发送请求时,服务器根据请求头的If-None-Match和If-Modified-Since字段来判断是否命中协商缓存,如果命中,则返回304状态码并更新浏览器缓存有效期。
Last-Modified(响应头)和If-Modified-Since(请求头)(HTTP/1.0)
- 服务器返回资源最后修改时间:
Last-Modified:Wed, 21 Oct 2024 07:28:00 GMT - 发送请求时会将当前的
Last-Modified值作为If-Modified-Since字段内容,放在请求头中发送给服务器 - 服务器判断比较:对比资源的当前最后修改时间和If-Modified-Since值
- 如果时间一致,说明资源未修改,返回
304 Not Modified,不返回内容,使用本地缓存; - 如果时间不一致,说明资源已修改,返回
200,和新的资源。
缺点: 精度到秒,如果文件在1秒内改变,无法识别;文件内容可能只是被 touch 了一下,修改时间变了,但内容没变,这时也会导致缓存失效。
Etag(响应头)和If-None-Match(请求头)(HTTP/1.1 精准度优先级更高)
Etag类似于文件指纹,是资源唯一标识(如哈希值)- 服务器返回Etag:"abc123"
- 发送请求时会将当前的
Etag值作为If-None-Match字段内容,放在请求头中发送给服务器 - 服务器判断比较:对比资源当前的Etag值和If-None-Match值
- 如果内容一致,说明资源未修改,返回304 Not Modified,不返回内容,使用本地缓存;
- 如果内容不一致,说明资源已修改,返回200,和新的资源。
缓存流程
- 用户输入URL,浏览器发起请求
- 浏览器检查本地是否有缓存
- 无缓存,直接向浏览器请求,流程结束
- 有缓存判断是否存在强缓存(
Expries和Cache-Control) - 判断强缓存是否过期,未过期:使用本地缓存(disk cache/memory cache),已过期:进入协商缓存
- 发起请求,请求头携带
If-Modified-Since和If-None-Match与服务器进行对比 - 服务器判断: 资源未变,返回304、浏览器继续使用缓存;资源改变,返回200和新内容,更新缓存。
缓存位置
- Memory Cache: 内存中的缓存,速度快,关闭标签后清除
- Disk Cache: 硬盘中的缓存,持久化,容量大
- Service Worker Cache: 运行在浏览器背后的独立线程,无法直接访问DOM,可用作离线缓存(PWA)、消息推送和网络代理,传输协议必须是HTTPS
- Push Cache: (推送缓存)短暂存在于连接中的缓存
强制刷新与缓存行为
- 普通刷新(F5刷新/点刷新按钮): 不使用强缓存,但可能使用协商缓存;如果匹配的话memory cahce可用的会被优先使用,其次是disk cache。
- 强制刷新(Ctrl+F5/Shift+Refresh): 浏览器不使用缓存,强制从服务器中获取
- 清空缓存并硬刷新: 彻底清除本地缓存
二、常见的HTTP请求头和响应头
请求头由客户端发送给服务器,包含了关于客户端环境、期望的响应格式等信息。
| 请求头 | 作用说明 |
|---|---|
| Accept | 告诉服务器客户端能接收的内容类型,如:text/html,application/json |
| Accept-Encoding | 客户端支持的压缩格式如gzip,有效减少传输体积 |
| Accept-Language | 客户端偏好的语言,如zh-CN,en-US, 用于国际化内容返回 |
| Content-Type | 表示请求体的数据格式如:application/json,multipart/form-data |
| Authorization | 携带身份认证信息,如Bearer 〈token〉 |
| Referer | 标明当前请求是从哪个页面跳转来的,用于防盗链或日志分析 |
| Origin | 表示跨域请求的源(协议+域名+端口),用于CORS判断是否允许跨域 |
| Cookie | 自动携带与当前域名匹配的Cookie,用户维持会话状态:登录态 |
| Host | HTTP/1.1规范中唯一一个必须的请求头,指定请求的服务器的域名和端口号 |
| Cache-Control | 控制静态资源缓存策略 |
| If-Modified-Since/If-None-Match | 用于协商缓存,判断资源是否更新 |
响应头
| 响应头 | 作用说明 |
|---|---|
| Cache-Control | 指定缓存策略的核心指令。例如 max-age=3600 表示资源可缓存1小时,no-cache 表示使用前必须向服务器验证,no-store 表示禁止缓存。 |
| Expires | 指定资源的绝对过期时间(GMT格式)。如果 Cache-Control 中的 max-age 存在,则优先使用 max-age。 |
| Accept-Language | 客户端偏好的语言,如zh-CN,en-US, 用于国际化内容返回 |
| Etag | 资源的唯一标识符(如哈希值)。浏览器下次请求时通过 If-None-Match 发送此值,服务器可据此判断资源是否变更,决定返回 200 OK 还是 304 Not Modified。 |
| last-Modified | 资源最后一次被修改的日期和时间(GMT格式)。浏览器下次请求时通过 If-Modified-Since 发送此值进行验证。 |
| Content-Type | 指明响应体的MIME类型和字符编码。例如 text/html; charset=utf-8、application/json、image/png。 |
| set-Cookie | 向浏览器设置一个Cookie。可包含 Secure(仅HTTPS传输)、HttpOnly(禁止JavaScript访问)、SameSite(限制跨站请求)等安全属性。 |
| Date | 响应消息生成的日期和时间(GMT格式) |
| Connection | 控制网络连接。keep-alive 表示保持TCP连接以便复用;close 表示发送完响应后关闭连接。 |
| Access-Control-Allow-Origin | CORS(跨域资源共享)头部,指定哪些外部域可以访问该资源。例如 https://example.com 或 *(通配符,允许所有,但有安全风险)。 |
三、HTTP和HTTPS
HTTP
HTTP是超文本传输协议,基于TCP/IP通信协议传递数据,无状态,不连接,使用明文传输。
HTTPS
HTTPS是基于SSL/TLS协议对数据进行加密传输的网络协议,比HTTP安全。
| 特性 | HTTP | HTTPS |
|---|---|---|
| 安全性 | 不安全,数据以明文形式传输,容易被窃取和篡改 | 安全,通过SSL/TLS协议对数据进行加密传输 |
| 加密机制 | 无加密 | 使用SSL/TLS证书进行非对称加密(握手阶段)和对称加密(数据传输阶段) |
| 默认端口 | 80 | 443 |
| 所需证书 | 不需要 | 需要由受信任的证书颁发机构(CA)签发的SSL/TLS证书 |
| URL前缀 | http:// | https:// |
| OSI网络模型 | 应用层 | 传输层 |
四、说说HTTP1.0、HTTP1.1、HTTP2.0、HTTP3.0的区别
HTTP1.0
HTTP协议最早的版本,明显存在性能问题。
核心特点:
无状态、短连接:每次请求都需要跟服务器建立一个TCP连接,服务器完成请求后会立即断开。如果一个页面需要加载10个资源(HTML、CSS、JS、图片),浏览器就需要建立10次独立的TCP连接。高延迟:每次建立TCP连接都需要“三次握手”,关闭连接需要“四次挥手”,这个过程消耗大量时间,导致页面加载速度变慢。功能简单:只能支持基本的请求方法(GET、POST、HEAD)。支持持久连接的雏形:通过非标准的Connection:keep-alive头,提示服务器不要立即关闭连接,但这并不是默认行为。
HTTP1.1
HTTP1.1是HTTP1.0的标准化和重大改进,它解决了1.0的许多核心性能问题。 核心特点:
持久连接:默认情况连接在完成之后不会被立即关闭,而是保持打开状态,在一个TCP连接上可以传送多个HTTP的请求和响应。管道运输:允许客户端在同一个连接上连续发送多个请求,这存在队头阻塞问题,如果第一个请求的响应很慢,它会阻塞后面所有请求的响应。管道化在实际中被默认禁用。冗余头部:每个请求都携带大量重复的Header(Cookie、User-agent等),导致带宽浪费。缓存控制:引入了更精细的缓存控制机制,如Cache-Control头、Etag等。更多请求方法:PUT、DELECT、OPTIONS等方法。
HTTP2.0
核心特点:
二进制分帧:http1.0/1.1使用纯文本格式,2.0采用二进制格式传输数据,将请求和响应分解成更小的二进制帧,如HEADERS帧、DATA帧。多路复用:基于二进制分帧,一个TCP连接上可以并行发送和接收多个请求和响应,且它们互不干扰,彻底解决了HTTP1.1队头阻塞的问题。头部压缩:使用HPACK算法对HTTP(请求和响应)头部进行压缩,显著减少了头部信息的大小,降低传输开销。服务器推送:允许服务器主动推送它认为客户端需要的资源,而无需客户端请求。 新的挑战: TCP层的队头阻塞:HTTP2.0虽然解决了应用层的队头阻塞,但底层仍依赖TCP,TCP是按顺序传输字节流的协议,如果一个TCP包丢失,所有后续的包都必须等待这个丢失的包重传成功,这被称为“TCP层的队头阻塞”
HTTP3.0
为了解决http2.0遗留下的TCP层的队头阻塞问题。 核心改革:
基于UDP协议:弃用TCP、拥抱QUIC,QUIC运行在UDP之上,UDP本身是无连接、不保证可靠性的协议。彻底解决TCP层队头阻塞:QUIC在协议层原生支持多路复用,每个数据流(Stream)都是独立的,如果一个流发生包丢失,只会阻塞该流本身的数据重组,其他流可以继续传输。更快的连接建立: TCP+TLS问题:建立一个安全的HTTP/2连接需要一次TCP三次握手(1RTT)和一次TLS握手(1-2RTT),总共需要2-3个RTT延迟。 QUIC解决方案:对于新客户端,QUIC可以在1个RTT内完成连接和加密,对于曾经连接过的客户端,QUIC支持0RTT快速重连,客户端可以在第一个数据包中就发送应用数据,极大减少了延迟。连接迁移:当用户的网络发生变化时,TCP连接会中断,需要重新建立。QUIC使用唯一的连接ID来标识连接,而不依赖IP地址和端口,因此,当设备IP改变时,只要连接ID不变,QUIC连接可以无缝在新的网络上继续。内置加密:QUIC从设计之初就强制要求加密,所有的头部和数据都是加密的,安全性更高。
五、地址栏输入URL敲下回车后发生了什么?
1. URL解析:
浏览器判断输入的内容是URL还是关键词,如果是关键词交给默认搜索引擎,如果是URL,会对完整的URL进行解析,分解出协议、主机(域名)、端口、路径、参数等。
2. DNS解析:
-
计算机在网络中通过IP地址进行通信,需要将www.google.com转换成对应的IP地址判断是正确的URL格式之后,DNS会在缓存中查询是否有当前域名的IP地址。 基本步骤:浏览器缓存(浏览器检查是否在缓存中)---〉操作系统缓存(操作系统DNS缓存,去本地的hosts文件查找)---〉路由器缓存(路由器DNS缓存)---〉ISP DNS缓存(在客户端设置的首选DNS服务器,又称本地的DNS服务器)
递归查询:如果ISP的DNS服务器也不知道,它会向根域名服务器、顶级域名服务器(.com)、权威域名服务器发起递归查询,最终找到负责google.com的服务器并获取IP地址。
3. 建立TCP连接:
浏览器使用获得的IP地址和端口号,通过操作系统协议发起TCP连接。通常使用的是HTTPS协议,在TCP连接的基础上还需要建立安全的TLS连接。
三次握手:
- SYN: 客户端发送一个SYN(同步)包到服务器,表示“想要和你建立连接”,并附带了一个随机的序列号(Seq=X)。
- SYN-ACK: 服务器收到后,回复一个SYN-ACK(同步-确认)包,表示“我收到了你的请求,我也想和你建立连接”,。包含自己的序号(Seq=Y)和对客户端序列号的确认(ACK=X+1)
- ACK: 客户端收到后,再回复一个ACK(确认)包,确定收到了服务器的SYN,包含确认号(ACK=Y+1),连接正式成立。
TLS握手(仅对HTTPS):
-
第一次握手:客户端发送请求Client Hello
告知支持的协议版本和加密方法,还有客户端生成的
随机数(client random),用于生成对话密钥。 -
第二次握手:服务器回复Server Hello
服务器确认使用的加密通信协议版本,确认使用的加密方法,发送
SSL证书、公钥以及一个随机数(Server random)。客户端验证证书的合法性(颁发机构、有效期、域名是否匹配等),如果这三个任何一个环节发现问题,浏览器会向访问者显示一个警告,由其选择是否还要继续通信。如果证书受信任,或用户接受了不受信任的证书,浏览器会生成一串新的
随机数(Premaster secret)。 -
第三次握手:客户端回应
此前,浏览器根据前三次握手中的随机数Client random、Server random、Premaster secret通过一定的算法来生成“会话密钥(Session Key)”,这个会话密钥就是接下来双方进行对称加密解密使用的密钥。
-
第四次握手:服务端回应
服务端收到客户端的回复,利用已知的加密解密方式进行解密,服务器收到客户端的第三个随机数(Premaster secret)之后,使用同样的算法计算出“会话密钥”(Session Key)。 至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就是完全使用普通的HTTP协议,只不过用“会话密钥”加密内容。
4. 发送HTTP请求:
TCP连接建立之后,浏览器会通过这个连接向服务器发送一个HTTP请求报文。 请求报文由请求行(GET/HTTP/1.1)、请求头Headers(Host/User-Agent/Accept/Connection:keep-alive等)、请求体(Body)等。
5.服务器处理请求并返回响应:
服务器接收到请求后,会根据请求的路径或方法进行处理,处理过程包括:读取静态文件(HTTML、CSS、JS、图片等)、查询数据库、执行应用程序逻辑等。
处理完成后,服务器会生成一个HTTP响应报文并发送回浏览器。
一个典型的响应报文包括:状态行(HTTP/1.1 200 OK)、响应头(Content-type:text/html; charset=UTF-8;Set-cookie;Cache-control:max-age:3600)、响应体(通常是HTML代码)。
6.浏览器解析和渲染页面:
浏览器将收到的HTML、CSS、JAvaScript等资源转换成用户可见的网页。
- 解析HTML,构建DOM树 DOM Tree 浏览器会从上到下解析HTML文档生成DOM树;
- 解析CSS,构建CSSOM树 CSSOM Tree
浏览器解析遇到样式时,会进行
异步下载,下载完成后构建CSSOM树;它包含所有元素的样式信息。 - 构建渲染树Render Tree 浏览器将DOM树和CSSOM树合并,生成一个渲染树;渲染树只包含需要在页面上显示可见的元素以及计算后的样式(display:none)的元素不会在渲染树中。 link不会阻塞DOM树构建过程,但会阻塞渲染树构建过程。
- 布局Layout 根据渲染树将DOM节点每一个节点布局在屏幕的正确位置上。
- 绘制Paint 浏览器将渲染树的每个像素绘制到屏幕上,填充颜色、边框、阴影、图片等视觉内容。
- 合成与显示 现代浏览器会将页面分成多个图层,分别绘制,各层绘制结果合并成一幅完整的画像,最终显示在用户的屏幕上。
7.断开连接(四次挥手):
数据传输完成后,为了释放资源,TCP连接需要被关闭。 这个过程被称为“四次挥手”:
- 客户端发送FIN包,表示“我这边数据发完了,准备关闭”。
- 服务器回复ACK包,表示“我收到了你的关闭请求”。
- 服务器发送FIN包,表示“我这边也发完了,准备关闭”。
- 客户端回复ACK包,连接正式关闭。
六、回流和重绘
回流(重排Reflow)
第一次确定节点的大小和位置,称为布局(layout),之后对节点的大小和位置修改重新计算称之为回流。
什么情况下会引起回流?
- DOM结构发生改变(添加新的节点或者移除节点)
- 改变了布局(修改了width、height、padding、font-size等值)
- 浏览器窗口大小改变(resize)
- 计算某些布局相关的属性(
offsetTop,offsetHeight,clientWidth,scrollWidth等) - 调用getComputedStyle方法获取尺寸、位置信息
- 激活CSS伪类(:hover)
重绘(RePaint)
当渲染树中的一些元素需要更新,而这些属性只是影响元素的外观、风格而不影响布局的,这个过程称为重绘。
什么情况下会引起重绘?
修改背景色、文字颜色、边框颜色、样式等。
回流一定会引起重绘,因为回流会重新计算布局,然后重新绘制。
如何在开发中避免不必要的回流和重绘?
- 使用class集中改变样式,避免逐个修改样式,最好一次性更改className或cssText。
- 脱离文档流进行修改
- 尽量避免通过getComputedStyle获取尺寸、位置等信息
- 避免频繁读取布局信息
- 使用
transform和opacity实现动画,这两个属性可以在合成 (Compositing) 阶段由GPU处理,通常只会触发合成,而不会触发重绘或回流,性能最佳。
七、浏览器资源解析
Script元素
当浏览器在解析HTML时,遇到script元素,停止继续构建DOM树,下载script代码并执行后,才会继续解析HTML,构建DOM树。
async属性的Script(异步执行,仅对外部脚本有效)
浏览器解析到带async属性的script时,不会中断HTML解析,而是并行下载脚本,当脚本下载完成后,中断解析并执行脚本;一旦加载完立即执行。
如果有多个js脚本,async标记的脚本哪个先下载结束,就先执行哪个脚本。
defer属性的Script(延迟执行,仅对外部脚本有效)
浏览器解析到带defer属性的script时,不会中断HTML解析,而是并行下载脚本,当浏览器解析完HTML时、在DomContentloaded事件之前执行下载完成的脚本。
如果有多个js脚本,defer标记的脚本按js脚本书写顺序执行。
同时使用async和defer属性,defer不起作用,浏览器行为由async属性决定。
浏览器解析不同资源时行为
- 遇到
CSS样式资源,CSS会异步下载,不会阻塞浏览器构建DOM树,但是会阻塞渲染,在构建渲染树时,会等CSS下载解析完毕后才进行; - 遇到
JS脚本资源时,需要等待JS脚本资源下载并执行后才会继续解析HTML;如果JS脚本加上async会异步执行,defer会是延迟执行; CSS加载会阻塞后面的JS语句的执行,因为HTML中有一项规定,浏览器在执行Script脚本前,必须保证外联的CSS样式解析完成,因为JS会去获取或者变更DOM的CSS样式,如果外联的CSS没解析好,获取到的结果是不准确的。- 遇到
Img图片时,直接异步下载,不会阻塞解析;下载完毕后用图片替换原有的src的地方。
Load和DOMContentLoaded区别
Load事件触发代表页面中的DOM、CSS、JS,图片已经全部加载完毕。DOMContentLoaded事件触发代表初始的HTML被完全加载和解析,无需等待CSS、带async的JS、图片加载。
八、浏览器安全
XSS
XSS(跨站脚本攻击)是指攻击者在返回的HTML中嵌入javascript脚本,从而拿到用户的信息(cookie)并进行操作。xss分为存储型、反射型和文档型。
存储型
攻击者将恶意脚本存储到网站的数据库中,当其他用户正常访问包含这些内容的页面时,恶意脚本就会从服务器加载并执行。
常见场景:一个博客的评论系统,攻击者提交脚本代码,服务器未过滤,将这条评论存入了数据库,之后所有访问这篇博客的用户,在加载评论时都会执行这个恶意脚本。
特点:危害最大,是持久性的,所有访问该页面的用户都会收到影响。
反射型
将恶意脚本作为请求URL的参数,经过服务器解析响应,拼接在HTML中传回客户端,然后浏览器解析执行恶意脚本。 特点:是一次性的,仅对惦记链接的用户生效,恶意脚本不存储在服务器上。 服务器不会存储这些恶意脚本。
文档型
攻击的Payload在客户端(浏览器)修改DOM环境时发生,不经过服务器。恶意代码的注入和执行都是在浏览器通过javascript操作DOM完成的。
特点: 整个过程在客户端完成,服务器响应本身可能是安全的。
XSS的防御
对输入进行转义和过滤:对引号、尖括号、斜杠进行转义,让代码在html解析的过程中无法执行;过滤就是把script标签删除。CSP:内容安全策略,通过HTTP头告诉浏览器只允许加载指定来源的脚本、样式等资源,从根本上杜绝内联脚本和未经允许的外部脚本的执行。设置HttpOnly:cookie设置httpOnly后,会禁止javascript脚本访问cookie,有效缓解cookie被盗。
CSRF
CSRF(跨站请求伪造)就是黑客诱导用户跳转恶意网站,然后利用用户的登录态发起恶意请求;原理就是http请求会自动携带cookie,而且是HTTP目标请求域名的cookie。
特点
- 攻击者并不能拿到用户的cookie,只是冒用。
- 整个过程用户可能完全不知情。
- 核心前提是用户必须已经登录目标网站。
CSRF的防御
Anti-CSRF Token:服务器会在用户会话中生成一个随机的、不可预测的Token。这个令牌必须包含在每一个状态改变的请求中,服务器收到请求后,必须验证这个令牌是否有效且匹配当前会话。因为攻击者无法从目标网站中获取这个动态令牌,所以伪造的请求会因缺少或错误的令牌而被拒绝。检查SameSite Cookies:该属性表示Cookie不随着跨域请求发送,可以很大程度减少CSRF的攻击;SameSite=Strict:完全禁止第三方cookie,SameSite=Lax:宽松模式,在大多数跨站子请求(图片加载、iframe)中不发送cookie,但在顶级导航(如从外部链接点进来)时会发送。验证Referer/Origin头:服务器检查请求头中的Referer或Origin字段,判断是否来自合法的源(自己的网站域名),但这种方法不可靠,Referer头会被伪造。
九、跨域
同源策略
- 只有
协议、域名、端口都相同的叫同源策略。只要有一个不同,就是跨域。
哪些行为受同源策略的限制
Cookie、LoacalStorage和IndexDB无法读取。(访问存储在浏览器中的数据,如localStorage和IndexDB市以源进行分割。每个源都拥有自己单独的存储空间)DOM无法获得。AJAX请求不能发送。
跨域解决方法
1.CORS(Cross-Origin-resource-Sharing)跨域资源共享
它是一种服务器端的授权机制,服务器通过在HTTP响应头中添加特定的字段,来告诉浏览器:我允许X源的请求访问我的资源。由后端设置响应头(Access-Control-Allow-Origin)来授权跨域访问。
CORS请求步骤:
当我们发起跨域请求时,如果是复杂请求,浏览器会帮我们自动触发预检请求(OPTIONS),用于确认目标资源是否支持跨域。如果是简单请求,则不会触发预检,直接发出正常请求。
浏览器会根据服务端响应的header(Access-Control-Allow-Origin)进行判断,如果响应支持跨域,则继续发出正常请求,如果不支持,则在控制台显示错误。
2.JSONP(JSON with Padding)
利用script标签没有跨域限制的特性。通过script标签指向一个需要访问的地址并提供一个回调函数来接收数据。
JSONP的缺点:
只支持GET请求;存在XSS风险;错误处理困难。
3.postMessage
window.postMessage()可以安全的实现跨域通信,从一个窗口向另一个窗口发送消息。这个方法接收两个参数:你想发送的数据(Message)和目标窗口的源(taegetOrigin)。
4.代理服务器(proxy)
在开发环境或生产环境中使用;原理:在客户端和目标服务器之间架设一个“中间人”。
开发环境: Webpack/Vite等构建工具内置的开发服务器可以配置代理。 前端请求http://localhost:3000/api/users, 开发服务器将其代理转发到http://localhost:5000/api/users, 对浏览器来说是同源请求。
生产环境:使用Nginx反向代理服务器。将/api/* 的请求代理到后端服务器。
十、Cookie、 localStorage、sessionStorage
Cookie
一开始设计的时候并不是用来做数据存储的,而是为了让HTTP具有状态;比如登录的标识存在cookie里,请求时就会自动携带cookie,这让无状态的HTTP请求变得能够标识请求的状态(身份);
Cookie和session的区别
两个都可以用来存私密的东西,比如用户身份,但是cookie数据保存在客户端,session数据保存在服务器端。
Cookie有效期
可通过Expires和Max-Age两个属性来设置。Expires是过期时间;Max-Age用的是一段时间间隔,单位是秒,从浏览器收到报文开始计算。
过期时间如果设置为负数和0,则浏览器关闭直接被销毁。
Cookie、 localStorage、sessionStorage区别
| 特性 | cookie | localStorage | sessionStorage |
|---|---|---|---|
| 存储大小 | 约4KB | 约5-10MB | 约5-10MB |
| 生命周期 | 可设置过期时间Expries/Max-Age | 永久存储,除非手动删除 | 页面会话期间有效 |
| 与服务器通信 | 每次都会在请求头中携带 | 不参与服务器通信 | 不参与服务器通信 |
| 访问权限 | 所有同源窗口共享 | 所有同源窗口共享 | 仅当前标签页共享 |
| 是否随请求发送 | 是,每次同域HTTP请求都会自动携带 | 否,需要JS获取 | 否,需要JS获取 |
| API | 通过document.cookie | setItem(key,value),getItem(key),removeItem(key),clear() | 同localStorage |
| 存储位置 | 浏览器和服务器 | 浏览器 | 浏览器 |
| 主要用途 | 识别用户、会话管理(保持用户登录状态)、跟踪用户行为、个性化设置 | 存储大量持久化数据(如用户偏好、离线数据) | 存储临时会话数据(如表单、草稿等) |
十一、进程和线程
进程(Process)
进程是操作系统进行资源分配和调度的基本单位;可以把它看成一个正在运行的程序实例。
核心特点
1. 独立的内存空间: 每个进程都有自己独立的虚拟地址空间(包括代码段、数据段、堆、栈等。)一个进程不能直接访问另一个进程的内存数据。
2. 资源拥有者: 进程拥有系统资源,如打开的文件、网络连接、CPU时间、内存等。
3. 开销大: 创建、销毁和切换进程需要消耗较多的系统资源(CPU和内存)。
线程(Thread)
线程是程序执行的最小单位,也称是CPU调度的基本单位;线程被包含在进程之中,一个进程中可以并发多个线程,每个线程并执行不同的任务。
核心特点
1. 共享进程资源: 同一个进程内的所有线程共享该进程的内容空间和系统资源。
2. 独立的执行流: 每个线程有自己的程序计数器、寄存器集合和栈,保证了每个线程可以独立的执行代 码。
3. 开销小: 创建、销毁和切换线程的开销远小于进程,因为它们共享内存空间。
为什么需要线程?
1. 提高响应性: 在图形界面程序中,一个线程处理UI,另一个线程处理耗时操作。
2. 资源共享: 线程共享内存,通信更加高效。
3. 经济性: 创造线程比创造进程的开销小。
打个比方:进程像独立的工厂,线程像工厂里的流水线。工厂之间相互独立,但工厂内的流水线共享资源、协同合作。
JS 为什么是单线程的
JavaScript作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
浏览器是多进程的优点
- 默认新开 一个
tab页面新建一个进程,所以单个tab页面崩溃不会影响到整个浏览器。 - 第三方插件崩溃也不会影响到整个浏览器。
- 多进程可以充分利用现代
CPU多核的优势。
十二、浏览器事件循环机制
JavaScript是单线程的,但是单线程会遇到“阻塞”问题(比如网络请求、定时器卡住主线程),事件循环就是为了解决单线程的异步问题,让js能够非阻塞地处理任务。
事件循环(Event Loop)执行顺序
- 执行调用栈中的所有同步代码;
- 检查微任务队列,执行所有的微任务(按顺序执行);
- 微任务执行完毕后,浏览器判断是否需要渲染(比如DOM的变化);
- 从宏任务队列中取下一个宏任务执行,重复上述步骤(循环)。
异步代码执行顺序规则
- 同步代码优先执行
- 微任务在宏任务之前执行
- 同类型任务按添加顺序执行
- 每个宏任务执行完后都会清空微任务队列
任务队列和微任务队列
任务队列:存放宏任务,如setTimeout/setInterval、setImmediate(IE/Node)、I/O操作;
微任务队列: 存放微任务,promise.then/catch/finally、 async/await(本质是Promise)、 process.nextTick(Node.js)、MutationObserver(监听DOM变化)。
注: new Promise构造函数本身是同步执行的,then()/catch()/finally()方法是异步的。
事件循环题目测试
console.log('start');
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
return Promise.resolve().then(() => {
console.log('async2 then');
});
}
setTimeout(() => {
console.log('setTimeout1');
new Promise((resolve)=> {
resolve();
}).then(()=>{
console.log("timeout1 promise");
setTimeout(()=>{
console.log("setTimeout2");
},0);
});
}, 0);
async1();
Promise.resolve()
.then(()=>{
console.log("promise1");
return new Promise((resolve)=>{
setTimeout(()=>{
resolve("timeout2 promise");
console.log("timeout3 promise");
},0);
});
})
.then((res)=>{
console.log(res);
});
console.log('end');
输出结果为
start
async1 start
async2
end
async2 then
async1 end
promise1
setTimeout1
timeout1 promise
timeout3 promise
timeout2 promise
setTimeout2
这段代码我们按照事件循环的同步代码→微任务→宏任务规则,结合async/await的底层逻辑、Promise 状态变化,一步步拆解执行顺序:
执行步骤:
1. 全局同步代码执行
-
console.log('start')→ 输出:start -
定义
async1/async2函数(仅声明,不执行) -
setTimeout(...)(第一个定时器):回调作为宏任务 1加入宏队列 -
调用
async1():-
console.log('async1 start')→ 输出:async1 start -
执行
await async2():- 调用
async2()→console.log('async2')→ 输出:async2;async2返回Promise.resolve().then(...),此then回调(console.log('async2 then'))加入微任务 1 await暂停async1,需等待async2返回的 Promise 完成后,async1后续代码(console.log('async1 end'))才会入微队列
- 调用
-
-
Promise.resolve().then(...):此then回调(内部逻辑)加入微任务 2 -
console.log('end')→ 输出:end
2. 同步代码执行完毕,处理微队列(按入队顺序)
-
微任务 1:
console.log('async2 then')→ 输出:async2 then此时async2的 Promise 完成,async1中await后的代码console.log('async1 end')加入微任务 3 -
微任务 2:执行回调逻辑:
console.log("promise1")→ 输出:promise1- 返回
new Promise((resolve)=>{setTimeout(...)}):内部setTimeout回调作为宏任务 2加入宏队列;此 Promise 为 pending,需等待setTimeout内resolve触发后才会 fulfilled,因此后续的.then((res)=>{console.log(res);})暂不加入微队列
-
微任务 3:
console.log('async1 end')→ 输出:async1 end
3. 微队列清空,处理宏队列 1(第一个 setTimeout 回调)
console.log('setTimeout1')→ 输出:setTimeout1new Promise((resolve)=>{resolve();}).then(...):此then回调加入微任务 4
4. 处理微队列 4
console.log("timeout1 promise")→ 输出:timeout1 promise- 内部
setTimeout(...)(第二个定时器):回调作为宏任务 3加入宏队列
5. 微队列清空,处理宏队列 2(Promise 内的 setTimeout 回调)
resolve("timeout2 promise"):使外层 Promise 变为 fulfilled,后续的.then((res)=>{console.log(res);})加入微任务 5console.log("timeout3 promise")→ 输出:timeout3 promise
6. 处理微队列 5
console.log(res)→ 输出:timeout2 promise
7. 微队列清空,处理宏队列 3(第二个定时器回调)
-
console.log("setTimeout2")→ 输出:setTimeout2