1 浏览器
1.1 强缓存和协商缓存
1️⃣ 强缓存(也称本地缓存)
使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不必再向服务器发起请求。
相关header字段
-
expires ------
HTTP1.0使用的expires
一个未来时间,代表资源的有效期,没有过期之前都使用当前资源。 -
cache-control------
HTTP1.1及其之后使用cache-control
-
max-age
: 当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。这是一个相对时间; -
no-cache
:不使用本地缓存。强制客户端在使用缓存前向服务器验证资源是否过期; -
no-store
:禁止浏览器缓存数据,也禁止保存至临时文件中,每次都重新请求; -
pubilc
:任何情况下都缓存(即使是HTTP认证的资源); -
private
:只能被终端用户的浏览器缓存,不允许CDN等中间层缓存服务器对其进行缓存;
-
为什么有了expries还要用cache-control
-
依赖客户端时间/相对时间
Expires
使用一个绝对的过期时间(如Expires: Wed, 21 Oct 2023 07:28:00 GMT
),这意味着它依赖于客户端(浏览器)的系统时间。如果客户端的时间不准确(例如,用户的设备时间设置错误),缓存机制可能会失效。Cache-Control
使用相对时间(如max-age=3600
,表示资源在 3600 秒后过期),不依赖于客户端的时间设置,因此更加可靠。
-
无法处理动态内容
- 对于动态生成的内容,
Expires
无法灵活地设置缓存策略,因为它只能设置一个固定的过期时间;
- 对于动态生成的内容,
-
不支持复杂的缓存控制
Expires
只能设置一个过期时间,无法实现更复杂的缓存策略(如no-cache
、no-store
等);cache-Control
可以控制客户端和中间代理服务器的缓存行为,而Expires
只能控制客户端缓存;
-
更好的兼容性
Cache-Control
是 HTTP/1.1 的标准字段,而Expires
是 HTTP/1.0 的字段。现代浏览器和代理服务器对Cache-Control
的支持更好。同时使用两者,可以确保在不支持Cache-Control
的环境中,仍然可以通过Expires
实现基本的缓存控制。如果两者同时存在,Cache-Control
的优先级更高。
2️⃣ 协商缓存(也称弱缓存)
如果命中强制缓存,我们无需发起新的请求,直接使用缓存内容,如果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会发挥作用了。
- 若已更新,则返回更新后的资源;
- 若没有更新,则返回304状态,告诉浏览器可直接使用本地缓存的资源,
相关header字段
-
响应头: Last-Modified
请求头: If-Modified-Since
( 资源修改的时间 )- 浏览器第一次发请求,服务器在返回的 respone 的 header 加上 Last-Modified, 表示资源的最后修改时间;
- 再次请求资源,在 requset 的 header 加上 If-Modified-Since , 值就是上一次请求返回的 Last-Modified 值;
- 服务器根据请求传过来的值判断资源是否有变化,没有则返回 304, 有变化就正常返回资源内容,更新 Last-Modified 的值 ;
- 304 从缓存加载资源,否则直接从服务器加载资源;
-
响应头:Etag
请求头:If-None-Match
(标识符字符串)- 一个标识符字符串,表示文件唯一标识,只要文件内容改动,ETag就会重新计算。缓存流程和 Last-Modified 一样;
为什么有了last modified还要用etag
-
精度问题
Last-Modified
使用时间戳(如Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
)来表示资源的最后修改时间。时间戳的精度通常只到秒级别,如果资源在秒级别内多次修改,Last-Modified
无法准确反映这些变化。ETag
是一个唯一的字符串标识符(如ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
),通常基于资源的内容生成(如哈希值)。只要资源内容发生变化,ETag
就会改变,因此它可以更精确地判断资源是否已更新。
-
内容未变但时间更新
- 如果资源的内容没有变化,但文件的修改时间被更新了(例如,文件被重新保存),
Last-Modified
会错误地认为资源已发生变化,导致不必要的资源传输。
- 如果资源的内容没有变化,但文件的修改时间被更新了(例如,文件被重新保存),
-
动态内容处理
Last-Modified
可能无法准确反映内容的变化,因为动态内容的修改时间可能不固定;ETag
可以基于内容的哈希值、版本号或其他唯一标识生成,因此适用于各种类型的资源(包括动态内容)。
-
更好的兼容性
- 客户端可以同时发送
If-Modified-Since
(基于Last-Modified
)和If-None-Match
(基于ETag
)请求头,服务器可以根据优先级(通常ETag
优先级更高)决定是否返回新资源。同时使用两者,可以确保在不支持ETag
的环境中,仍然可以通过Last-Modified
实现基本的资源验证。
- 客户端可以同时发送
1.2 刷新对于强缓存和协商缓存的影响
- 当ctrl+f5强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存。
- 当f5刷新网页时,跳过强缓存,但是会检查协商缓存。
- 浏览器地址栏中写入URL,回车 浏览器发现缓存中有这个文件了,不用继续请求了,直接去缓存拿。(最快)
1.3 实际项目中用到的缓存?
用 webpack 打包文件,除了 index.html 入口文件不缓存,其他静态文件用 hash 值命名,hash 值不变就走强缓存,否则就走协商缓存。
单页应用上线版本缓存控制
// nginx禁止缓存index.html文件
location ~* .(html)$ {
add_header Cache-Control no-store;
}
// js、css文件强制缓存一个月
location ~* .(js|css|map|jpg|png|svg|ico)$ {
add_header Cache-Control "public,max-age=2592000";
}
1.4 http同域名下的并发限制
浏览器为什么并发限制?
-
对操作系统端口考虑;
PC总端口为65536,一个TCP链接就占用一个端口;(http也是一个TCP)
操作系统通常会对总端口一半开放对外请求,防止端口数量不被迅速消耗殆尽;
-
过多并发导致频繁切换产生性能问题;
一个线程对应一个http请求,如果并发数量巨大的话会导致线程频繁切换,而线程的上下文切换有时候并不是一个轻量级的资源;
所以请求控制器里面会产生一个链接池,以复用之前的链接;(4 - 8个)
-
避免同一客户端并发大量请求超过服务端的并发阈值;
服务端通常会对同一客户端来源设置并发阈值避免恶意攻击;
优化手段
-
静态资源优化
- 将静态资源(如图片、CSS、JavaScript)分布到多个域名下,增加并发请求数。
- 使用 CDN 加速资源加载。
域名分片
一个ip映射出多个域名,将请求通过多个域名散开,建议4个域名左右,过多域名会导致dns解析性能问题;
方法步骤: 给定一组域名,这一组域名指向同一个源;静态文件的时候随即返回这组域名的其中一个;
注意:在 HTTP/2 中,域名分片不再推荐使用,因为 HTTP/2 支持多路复用(Multiplexing),可以更好地处理并发请求。
-
减少请求数量
- 合并 CSS 和 JavaScript 文件。
- 使用雪碧图(CSS Sprites)减少图片请求数量。
-
升级到 HTTP/2
- 如果服务器支持 HTTP/2,优先使用 HTTP/2,以充分利用多路复用的优势。
HTTP/2 支持 多路复用(Multiplexing) ,允许在同一个连接上同时发送多个请求和响应。
在 HTTP/2 中,浏览器的并发限制不再适用,因为所有请求都可以通过同一个连接并行处理。
-
预加载关键资源
- 使用
<link rel="preload">
预加载关键资源,确保它们优先加载。
- 使用
1.5 HTTP1 、HTTP2 的区别
HTTP/1.1
特点
- 文本协议:HTTP/1.1 是基于文本的协议,请求和响应头是纯文本格式。
- 持久连接(Keep-Alive) :支持持久连接,允许在同一个 TCP 连接上发送多个请求和响应。
- 队头阻塞(Head-of-Line Blocking) :虽然支持持久连接,但请求和响应必须按顺序处理,如果前面的请求被阻塞,后续请求也会被阻塞。
- 并发限制:浏览器对同一域名下的并发请求数有限制(通常为 6-8 个)。
- 无状态:每个请求都是独立的,服务器不会记住之前的请求。
优化手段
- 域名分片(Domain Sharding) :通过将资源分布到多个域名下,绕过浏览器的并发限制。
- 资源合并:将多个小文件(如 CSS、JavaScript)合并为一个文件,减少请求数量。
- 使用 CDN:通过 CDN 分发静态资源,加速加载速度。
缺点
- 性能瓶颈:由于队头阻塞和并发限制,HTTP/1.1 的性能较差,尤其是在加载大量小文件时。
- 冗余头部:每个请求和响应都包含完整的头部信息,导致带宽浪费。
HTTP/2
特点
- 二进制协议:HTTP/2 是基于二进制的协议,头部和数据帧都以二进制格式传输,解析效率更高。
- 多路复用(Multiplexing) :允许在同一个 TCP 连接上同时发送多个请求和响应,解决了队头阻塞问题。
- 头部压缩(HPACK) :使用 HPACK 算法压缩头部,减少冗余数据。
- 服务器推送(Server Push) :服务器可以主动向客户端推送资源,减少请求延迟。
- 流优先级(Stream Prioritization) :可以为不同的请求设置优先级,确保关键资源优先加载。
- 无并发限制:由于多路复用的特性,浏览器不再需要限制同一域名下的并发请求数。
2.2 优点
- 性能提升:多路复用和头部压缩显著减少了延迟和带宽消耗。
- 简化优化:不再需要域名分片和资源合并等优化手段。
- 更好的用户体验:服务器推送和流优先级可以加快页面加载速度。
2.3 缺点
- 实现复杂度:HTTP/2 的实现比 HTTP/1.1 更复杂,需要服务器和客户端都支持。
- TLS 依赖:虽然 HTTP/2 可以在非加密连接上运行,但大多数浏览器要求使用 HTTPS(TLS 加密)。
1.6 http、 https的区别
主要区别在于 安全性,HTTPS 通过加密和身份验证机制提供了更高的安全性;
主要区别对比
特性 | HTTP | HTTPS |
---|---|---|
安全性 | 无加密,明文传输 | 加密传输,防止窃听和篡改 |
身份验证 | 无 | 通过 SSL/TLS 证书验证服务器身份 |
默认端口 | 80 | 443 |
性能 | 较高 | 较低(但现代优化已减少开销) |
SEO 影响 | 劣势 | 优势 |
用户体验 | 标记为“不安全” | 显示“锁”图标,增强信任 |
配置复杂度 | 简单 | 较复杂(需配置证书) |
安全性
-
HTTP
- 无加密:数据以明文形式传输,容易被窃听或篡改。
- 无身份验证:无法验证服务器的身份,容易受到中间人攻击(Man-in-the-Middle Attack)。
-
HTTPS
- 加密传输:通过 TLS/SSL 协议对数据进行加密,防止数据被窃听或篡改。
- 身份验证:通过 SSL/TLS 证书验证服务器的身份,确保用户访问的是合法的服务器。
协议和端口
-
HTTP
- 协议:基于 TCP 的应用层协议。
- 默认端口:80。
-
HTTPS
- 协议:在 HTTP 和 TCP 之间加入了 TLS/SSL 加密层。
- 默认端口:443。
性能
-
HTTP
- 性能较高:由于没有加密和解密过程,传输速度较快。
- 适合场景:对安全性要求不高的场景(如静态网站、内部网络)。
-
HTTPS
- 性能开销:加密和解密过程会消耗一定的 CPU 资源,但现代硬件和优化技术(如 TLS 1.3)已经大大减少了性能开销。
- 适合场景:对安全性要求高的场景(如登录、支付、数据传输)。
SEO 和用户体验
-
HTTP
- SEO 劣势:搜索引擎(如 Google)会降低 HTTP 网站的搜索排名。
- 用户体验:浏览器会标记 HTTP 网站为“不安全”,降低用户信任度。
-
HTTPS
- SEO 优势:搜索引擎会优先收录 HTTPS 网站,并提高其搜索排名。
- 用户体验:浏览器会显示 HTTPS 网站的“锁”图标,增强用户信任。
配置复杂度
-
HTTP
- 配置简单:无需配置证书或加密。
- 适合场景:快速搭建测试环境或内部网络。
-
HTTPS
- 配置复杂:需要获取和配置 SSL/TLS 证书。
- 适合场景:生产环境或对外服务的网站。
1.7 什么是预检请求
为什么会有预检请求?
预检请求的发生,来源于浏览器的跨域请求,浏览器对跨域的处理方式一般有两种:
- 浏览器限制客户端发起跨域请求
- 跨域请求正常发起,但是服务器返回的结果被浏览器拦截
一般情况下,跨域产生是第二种情况,服务器对数据已经进行了处理然而结果被浏览器拦截了,造成请求失败。
所以为了避免这种情况,浏览器使用了 HTTP 的 OPTIONS 方法发起了一个预检请求,预检请求成功之后才会发起真实的带数据的请求,否则阻止。
什么情况下发起预检请求?
CORS
分为两种请求:简单请求和非简单请求。
简单请求
GET
、POST
、HEAD
请求Content-Type
的类型:application/x-www-form-urlencoded
、multipart/form-data
、text/plain
HTTP
请求头:Accept
、Accept-Language
、Content-Language
、DPR
、Downlink
、Save-Data
、Viewport-Width
、Width
非简单请求
- 除了
GET
、POST
、HEAD
请求以外的其他请求 Content-Type
的类型:不属于简单请求的类型的以外的类型HTTP
请求头: 除简单请求以外的字段
当是非简单请求时,浏览器会先进行一次预检请求以确定能否正常访问,是一种对数据修改的保护。
1.8 从输入URL到看到页面发生的全过程
-
用户输入URL:用户首先在浏览器地址栏中输入想要访问的网站的URL。
-
浏览器解析URL:浏览器内部代码会解析这个URL。它首先会检查本地hosts文件,看是否有对应的域名。如果有,浏览器就会直接向该IP地址发送请求。如果没有,浏览器会将域名发送给DNS服务器进行解析,将域名转换成对应的服务器IP地址。
-
建立TCP连接:浏览器得到IP地址后,会通过TCP协议与服务器建立连接。TCP/IP协议是Internet的基础,它负责确保数据在网络中的可靠传输。这一过程中会进行三次握手,确保双方都已准备好进行通信。
-
发送HTTP请求:TCP连接建立后,浏览器会向服务器发送HTTP请求。这个请求包含了请求行(如GET方法、请求的URI、HTTP版本等)、请求头部(如Accept-Charset、Accept-Encoding等)以及可能存在的请求正文。
-
服务器处理请求:服务器收到请求后,会根据请求的内容进行相应的处理。这可能包括查询数据库、生成动态内容等。
-
发送HTTP响应:服务器处理完请求后,会发送一个HTTP响应给浏览器。这个响应包含了状态行(如HTTP版本、状态码、状态描述等)、响应头部(如Content-Type、Content-Length等)以及响应正文(即实际要显示的页面内容)。
-
浏览器解析和渲染页面:浏览器收到响应后,会解析响应正文中的HTML代码,并下载所需的CSS、JavaScript等资源文件。然后,浏览器会根据这些资源来渲染页面,最终将页面呈现给用户。
1.9 DNS如何将域名转换成对应的服务器IP地址
-
用户在浏览器中输入域名
- 用户在浏览器地址栏输入
www.example.com
。
- 用户在浏览器地址栏输入
-
检查本地缓存
- 浏览器首先检查自己的缓存中是否有
www.example.com
对应的 IP 地址。 - 如果找到,直接使用缓存结果,解析结束。
- 浏览器首先检查自己的缓存中是否有
-
检查操作系统缓存
- 如果浏览器缓存中没有,操作系统会检查自己的 DNS 缓存(如 Windows 的
hosts
文件或 DNS 缓存)。 - 如果找到,直接使用缓存结果,解析结束。
- 如果浏览器缓存中没有,操作系统会检查自己的 DNS 缓存(如 Windows 的
-
查询本地 DNS 服务器
- 如果本地缓存中没有,浏览器会向本地 DNS 服务器(通常由 ISP 提供)发送查询请求。
- 本地 DNS 服务器会检查自己的缓存,如果找到,返回结果。
-
迭代查询
- 根域名服务器:本地 DNS 服务器向根域名服务器(Root DNS Server)查询
.com
的顶级域名服务器地址。 - 顶级域名服务器:根域名服务器返回
.com
的顶级域名服务器地址,本地 DNS 服务器向.com
服务器查询example.com
的权威域名服务器地址。 - 权威域名服务器:顶级域名服务器返回
example.com
的权威域名服务器地址,本地 DNS 服务器向权威域名服务器查询www.example.com
的 IP 地址。 - 返回结果:权威域名服务器返回
www.example.com
的 IP 地址。
- 根域名服务器:本地 DNS 服务器向根域名服务器(Root DNS Server)查询
-
返回结果
- 本地 DNS 服务器将查询到的 IP 地址返回给浏览器,并缓存结果。
- 浏览器将 IP 地址缓存,并向该 IP 地址发送 HTTP 请求。
总结一下
- 浏览器缓存 → 2. 操作系统缓存 → 3. 本地 DNS 服务器 → 4. 根域名服务器 → 5. 顶级域名服务器 → 6. 权威域名服务器 → 7. 返回 IP 地址
递归查询和递归查询区别?
递归查询: 如果主机所询问的本地域名服务器不知道被查询域名的 IP 地址,那么本地域名服务器就以 DNS 客户端的身份,向其他根域名服务器继续发出查询请求报文,即替主机继续查询,而不是让主机自己进行下一步查询。
迭代查询: 当根域名服务器收到本地域名服务器发出的迭代查询请求报文时,要么给出所要查询的IP 地址,要么告诉本地服务器下一步应该找哪个域名服务器进行查询,然后让本地服务器进行后续的查询。
由于递归查询对于被查询的域名服务器负担太大,通常采用以下模式:从请求主机到本地域名服务器的查询是递归查询,而其余的查询是迭代查询。
1.10 首屏加载时间优化
2. css
2.1 清除浮动的原理
利用清除可以设置元素禁止浮动元素出现在他的左侧、右侧甚至是双侧
css2.1引入了清除区域,是在元素上外边距之上增加的额外间隔,确保浮动元素不会和该元素重叠,不允许浮动元素进入这个范围。
2.2 vertical-align属性值?在什么情况下生效?
线类:baseline, top, middle, bottom
文本类:text-top, text-bottom
上标下标类:sub, super
数值百分比类:20px, 20em, 20% 对于基线,正值往上、负值往下偏移
生效前题
1:内联元素
2:display: inline / inline-block / inline-table / table-cell;
3:浮动和绝对定位会让元素块状化,因此元素不会生效;
2.3 什么是em单位?
em是一个相对的度量单位,对于浏览器来说,1em=16px,16px为浏览器的默认字体大小。
相对的度量单位:em单位为一个相对的度量单位,它通过寻找父标签的font-size,然后通过计算得出自身的font-size。利用em单位设置便签的width或者height等属性原理也一样。
#p1{color: red;font-size:2em;}
#span1{color:green;font-size:2em;}
<p id="p1">
p便签内容
<br/>
<span id="span1">span便签内容</span>
</p >
#p1的font-size=16px(浏览器的默认字体大小)*2em=32px;
#span1的font-size=32px(父标签继承来的字体大小,如果没有,则为16px)*2em=64px;
2.4 绝对定位和浮动的区别
相同点:都会破坏文档流,都会产生浮动;
不同点:浮动时:文档流的其他内容不会忽略浮动的位置,其他内容会绕开;
绝对定位时:文档流的其他内容会忽略浮动的位置,会直接穿过绝对定位所在的div;
2.5 伪类 伪元素 区别?
伪类和伪元素的根本区别在于:它们是否创造了新的元素
伪类:存在DOM文档中,逻辑上存在但在文档树中却无须标识的“幽灵”分类。
用于向某些选择器添加特殊的效果
动态伪类::visited、:focus、:hover等
状态伪类::disabled、:empty、:required 等
结构伪类::first-child、:nth-of-type等
其他伪类::target、:lang、:not()等
伪元素/ 伪对象:不存在在DOM文档中,是虚拟的元素,是创建新元素。代表某个元素的子元素,这个子元素虽然在逻辑上存在,但却并不实际存在于文档树中。
用于将特殊的效果添加到某些选择器 根本意思就是就是对那些不能通过class、id等选择元素的补充
伪元素选择符 ::after ::before
2.6 CSS中哪些元素可以继承
1、字体系列属性
font:组合字体
font-family:规定元素的字体系列
font-weight:设置字体的粗细
font-size:设置字体的尺寸
font-style:定义字体的风格
2、文本系列属性
text-indent:文本缩进
text-align:文本水平对齐
line-height:行高
word-spacing:增加或减少单词间的空白(即字间隔)
letter-spacing:增加或减少字符间的空白(字符间距)
text-transform:控制文本大小写
direction:规定文本的书写方向
color:文本颜色
3、元素可见性:visibility
4、生成内容属性:quotes
5、光标属性:cursor
所有元素可以继承的属性
1、元素可见性:visibility
2、光标属性:cursor
内联元素可以继承的属性
1、字体系列属性
2、除text-indent、text-align之外的文本系列属性
块级元素可以继承的属性
1、text-indent、text-align
2.7 一行指定放4个元素
使用flex布局;
.parent {
display: flex;
flex-wrap: wrap;
}
.item {
// 1
flex: 1 1 25%;
max-width: 25%;
// 2
flex-basis: 25%
}
2.8 flex-basis和width的区别
-
作用范围
-
width
:- 适用于所有元素,无论是否在 Flexbox 布局中。
- 在 Flexbox 布局中,
width
会被flex-basis
覆盖(除非flex-basis
为auto
)。
-
flex-basis
:- 仅适用于 Flexbox 布局中的 Flex 项目。
- 它决定了 Flex 项目在主轴方向上的初始大小。
-
-
2.2 优先级
-
在 Flexbox 布局中,
flex-basis
的优先级高于width
。- 如果同时设置了
flex-basis
和width
,flex-basis
会覆盖width
。 - 如果
flex-basis
为auto
,则width
会生效。
- 如果同时设置了
-
-
2.3 默认值
-
width
:- 默认值为
auto
,即元素的宽度由其内容决定。
- 默认值为
-
flex-basis
:- 默认值为
auto
,即元素的初始大小由width
或内容决定。
- 默认值为
-
-
2.4 响应性
-
width
:- 设置固定宽度后,元素的宽度不会随 Flexbox 布局的变化而调整。
-
flex-basis
:- 设置
flex-basis
后,元素的初始大小会根据 Flexbox 布局的剩余空间进行调整。
- 设置
-
3. js
3.1 闭包
什么是闭包
能够访问自由变量的函数,自由变量指的是作用域外的变量(MDN解释);
或者说有权访问另一个函数作用域的变量的函数(红宝书解释);
简单来说:内部函数使用了外部函数的一个变量就形成了一个闭包;
闭包优缺点
优点:私有化数据在私有化数据的基础上保持数据;私有化数据就是把一些变量私有化到函数里面;
缺点:内存泄漏 -- 指的是内存真的泄露了,影响了应用程序的执行等;
需要进入垃圾回收机制 :关键词 标记清除算法,引用计数算法, 新生代老生代;
闭包使用
节流、防抖、柯里化、高阶函数、vue的响应式原理,react hook...
使用原因:使用闭包的时候不会在内存中消失,方便下次调用的时候能获取到上一次的数据;
衍生问题:内存数据不会主动清空,需要我们及时将这些变量置空,防止内存过多,内存爆掉;
3.2 内存泄漏
内存泄漏就是内存占用,内存占用很高,但没有被有效利用,会影响应用程序执行;
一般来说,函数创建就会申请占用内存,函数返回时,垃圾回收器会自动完成内存释放;
3.3 垃圾回收机制常用方法
1️⃣ 引用计数法(Reference Counting)
在变量引用的时候将变量的引用计数+1,当变量的引用失效时(如离开作用域或者手动删除)将计数-1,直到计数为0时将该变量销毁。
优缺点
-
优点:
- 发现垃圾立即回收;不需要等待垃圾回收周期;
-
缺点:
- 无法处理循环引用;如果两个对象互相引用,它们的引用计数永远不会为 0,即使它们已经不再被使用;
如何解决循环引用问题:
-
手动解除引用:
- 在不再需要对象时,手动将其属性设置为
null
;
- 在不再需要对象时,手动将其属性设置为
-
使用弱引用(WeakMap/WeakSet):
- 弱引用不会增加对象的引用计数,适合用于缓存或临时存储;
2️⃣ 标记清除法(Mark-and-Sweep)
这种算法会存在一个根节点(GC root),始终无法被回收;比如window对象,dom树根节点...;
依据: 可达性;
可达性是什么: 从GC root出发,能被访问到的对象都被视为活跃对象,其余对象就是可被回收对象;
核心思想就是将整个垃圾回收操作分为两个阶段:
-
标记阶段(Mark):
- 垃圾回收器从根对象(Roots)开始遍历。
- 从根对象出发,递归遍历所有可达的对象,并标记它们为“活动的”(即仍在使用的对象)。
-
清除阶段(Sweep):
- 遍历整个堆内存,找到所有未被标记为“活动的”对象。
- 这些未被标记的对象被认为是垃圾,回收器会释放它们占用的内存。
- 注意在这个阶段中也会把第一阶段涉及的标志给抹掉,便于GC下次能够正常的工作
优缺点
-
优点:
- 可以回收循环引用的对象空间;即使对象之间互相引用,只要它们不可达,就会被回收;
-
缺点:
- 暂停时间(Stop-the-World):
- 在标记和清除阶段,垃圾回收器需要暂停程序的执行(称为“全停顿”)。
- 对于大型应用,可能会导致明显的性能问题。
- 内存碎片
- 清除阶段会释放不连续的内存块,可能导致内存碎片
- 内存碎片会降低内存分配效率。
- 暂停时间(Stop-the-World):
空间碎片化: 所谓空间碎片化就是由于当前所回收的垃圾对象,在地址上面是不连续的,由于这种不连续造成了在回收之后分散在各个角落,造成后续使用的问题;
3️⃣ 标记整理法(Mark-and-Compact)
基于标记清除法,第1阶段(标记)一致,区别在第2阶段;
-
标记阶段(Mark)
- 从根对象(如全局对象、当前执行上下文中的变量等)开始,递归遍历所有可达的对象,并标记它们为“活动的”。
-
整理阶段(Compact) :
- 将所有存活的对象移动到内存的一端,使它们连续排列。
- 移动过程中,更新对象的引用地址。
-
清除阶段(Sweep) :
- 清除未被标记的对象,释放它们占用的内存。
优缺点
-
优点
-
减少内存碎片:
- 通过整理内存,使存活对象连续排列,减少内存碎片。
-
提高内存分配效率:
- 连续的内存空间可以更快地分配新对象。
-
适合老生代回收:
- 老生代中的对象生命周期较长,内存碎片问题更明显,标记整理法可以有效解决这一问题。
-
-
缺点
-
性能开销:
- 整理阶段需要移动对象并更新引用地址,增加了额外的开销。
-
暂停时间(Stop-the-World):
- 在标记和整理阶段,垃圾回收器需要暂停程序的执行,可能导致性能问题。
-
4️⃣ 分代回收(Generational Collection)
基于对象的生命周期特点,将内存划分为不同的“代”(Generations),并对不同代采用不同的垃圾回收算法。
分代回收的工作流程
新生代(Young Generation)
-
特点:
- 存放新创建的对象。
- 大多数对象很快就不再被使用。
-
回收算法:
-
通常使用 Scavenge 算法(一种复制算法)。
-
将新生代内存划分为两个区域:From 空间和To 空间。
-
新创建的对象首先分配到 From 空间。
-
当 From 空间满时,触发垃圾回收:
- 将 From 空间中存活的对象复制到 To 空间。
- 清空 From 空间。
-
交换 From 空间和 To 空间的角色。
-
-
优点:
- 回收速度快,适合处理大量短生命周期对象。
-
缺点:
- 只能使用一半的内存空间(因为需要划分 From 和 To 空间)。
老生代(Old Generation)
-
特点:
- 存放从新生代晋升过来的对象(即经过多次垃圾回收仍然存活的对象)。
- 对象的生命周期较长。
-
回收算法:
- 通常使用 标记清除法(Mark-and-Sweep) 或 标记整理法(Mark-and-Compact) 。
-
优点:
- 适合处理生命周期长的对象。
-
缺点:
- 回收速度较慢,可能会阻塞程序执行。
对象晋升(Promotion)
当新生代中的对象经过多次垃圾回收仍然存活时,会被晋升到老生代。
晋升条件:
- 对象在 From 空间和 To 空间之间复制多次后仍然存活。
- To 空间的使用率超过一定阈值(25%)。
优缺点
- 优点
-
高效利用内存:
- 针对不同生命周期的对象采用不同的回收策略,提高内存使用效率。
-
减少垃圾回收的开销:
- 大多数短生命周期对象在新生代中被快速回收,减少了对老生代的扫描和回收频率。
-
适应现代应用场景:
- 现代应用中,大多数对象的生命周期很短,分代回收能够很好地适应这种特点。
-
缺点
-
实现复杂:
- 需要维护多个内存区域,并实现不同的回收算法。
-
晋升机制的开销:
- 对象从新生代晋升到老生代需要额外的开销。
3.4 Event Loop
宏任务
- 新程序或子程序被直接执行,包括script元素里的代码,控制台代码...
- 时间的回调函数,比如鼠标点击触发后里面的回调函数...
- setTimeout(), setInterval()
- requestAnimationFrame, I/O操作,setImmediate, UI rendering...
微任务
- promise .then() .catch() .finally()
- MutationObserver
- Object.observe
- node.js 里的 process.nextTick()
运行顺序
事件循环是一个不断进行循环的机制,事件循环会不断寻找可以执行的任务来执行;
在执行完同步任务以后,也就是清空调用栈以后,首先会执行微任务队列的任务,微任务执行完后才会去执行宏任务;
宏任务开始 -》 微任务 -》渲染 -》下一轮宏任务 -》微任务 -》渲染 -》 宏任务 ...
当队列中没有微任务是时候,就可以一直清除宏任务;
3.5 Promise/A+规范
Promise/A+规范是一种用于异步编程的规范,定义了Promise对象的行为和交互方式。它解决了JavaScript中异步编程的一些常见问题,如回调地狱、难以管理的异步代码流程和错误处理等。
Promise/A+规范由ECMAScript标准化,并在ES6(ES2015)中被采用。
核心概念和术语
Promise/A+规范定义了以下几个核心概念:
Promise:一个有then方法的对象或函数,用于表示异步操作的结果。
Thenable:一个有then方法的对象或函数。
Value:Promise状态成功时的值,可以是任何数据类型,包括undefined、thenable或另一个Promise。
Reason:Promise状态失败时的值,表示拒绝的原因。
状态转换和回调执行规则
Promise/A+规范定义了Promise的三种状态:
Pending:初始状态,既没有被解决(fulfilled),也没有被拒绝(rejected)。
Fulfilled:表示操作成功完成。
Rejected:表示操作失败。
Promise的状态一旦确定,就不会再改变。
如果Promise已经成功(fulfilled),则执行onFulfilled回调,并将结果作为参数传递;
如果失败(rejected),则执行onRejected回调,并将失败原因作为参数传递
实现细节和关键特性
实现一个符合Promise/A+规范的Promise需要遵循以下关键特性:
then方法:接收两个参数,分别是onFulfilled和onRejected,这两个回调函数只有在Promise状态确定时才会执行,并且只能执行一次。
微任务执行:onFulfilled和onRejected回调应该在微任务阶段执行,通常使用queueMicrotask来实现。
多次调用then:Promise的then方法可以被调用多次,每次调用都会保存对应的回调函数,等待Promise状态确定后依次执行。
参数规范:onFulfilled和onRejected必须是函数类型,如果不是函数则应该被忽略。
3.6 promise除了then,还知道什么方法
resolve、reject、then、catch、finally、all、allSettled、race、any
Promise.all Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
Promise.all()全部子实例都成功才算成功,有一个子实例失败就算失败。
Promise.all([promise1, promise2]).then(success1, fail1)
`promise1`和`promise2`都成功才会调用`success1`
注意点:
- 如果所有的Promise中只有一个执行错误,那么整个Promise.all不会走Promise.all().then() 而是走Promise.all().catch()这个流程了。但是要注意的是虽然走到了Promise.all().catch()这个流程 ,但是其他 Promise 还是会正常执行,但不会返回结果。
- 要注意Promise.all()的返回值顺序,Promise.all().then()的返回值顺序和传入的顺序是一致的,笔试时 遇到手写Promise.all()时要注意。
Promise.race Promise.race()方法也是将多个 Promise 实例,包装成一个新的 Promise 实例。
Promise.race()rece是赛跑机制,要看最先的promise子实例是成功还是失败。
Promise.race([promise1, promise2]).then(success1, fail1)
`promise1`和`promise2`只要第一个成功就会调用`success1`
Promise.any Promise.any()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
Promise.any()有一个子实例成功就算成功,全部子实例失败才算失败。
Promise.race([promise1, promise2]).then(success1, fail1)
`promise1`和`promise2`只要有一个成功就会调用`success1`
总结:Promise.all() 方法是 && 的关系;Promise.any() 方法是 || 的关系; Promise.race()方法是 赛跑机制 ;
3.7 aysnc await 和 promise的区别
await 基于promose实现,使得异步代码看起来像同步代码
会等待异步代码执行,会阻塞代码(使用时要考虑性能)
async/await让代码调试变得简单
1)使用async函数可以使代码简洁很多,不需要像promise一样需要些then,不需要写匿名函数Promise的resolve值,也不需要定义多余的data变量,避免了嵌套代码。 2)错误处理:async/await让try/catch可以同时处理同步和异步错误。
3.8 js中为什么会出现变量提升
变量提升的原因
-
执行上下文创建阶段: JavaScript 引擎在执行代码前会先创建执行上下文(Execution Context)。在这个阶段,引擎会扫描代码,找到所有的变量和函数声明,并将它们存储在内存中。
-
变量声明与初始化的分离: 变量声明会被提升,但赋值操作不会。例如,
var a = 10;
会被拆分为var a;
(提升)和a = 10;
(留在原地)。
函数声明提升:
- 函数声明会被整体提升,包括函数体。因此,你可以在函数声明之前调用它。
注意事项
-
let
和const
的提升:let
和const
也会被提升,但在声明前访问会触发“暂时性死区”(Temporal Dead Zone,TDZ),导致报错。 -
函数表达式不会提升: 只有函数声明会被提升,函数表达式不会。
总结
变量提升是 JavaScript 引擎在代码执行前处理声明的方式,目的是在执行上下文中提前分配内存。理解提升有助于避免代码中的潜在问题。
4. React
4.1 JSX
是一个JavaScript的语法拓展,允许在JavaScript中编写类似HTML的代码,对组件的描述更具有可读性;
在JSX中,可以使用 {}
来嵌入 JS
表达式,实现动态渲染和逻辑控制;
4.2 组件状态state和属性props之间的区别
state:
组件内部定义的可被修改的变量;
只在组件内部使用,跟外部引用情况无关;
state改变时,组件会重新渲染;
props:
组件外部传入的不可修改的常量,状态只读;
想修改props的值,只能从父级组件修改;
父级的props改变时,子组件会重新渲染;
4.3 事件机制
事件注册、事件合成、事件冒泡、事件派发;
合成事件的触发是基于浏览器的事件机制来实现的,通过冒泡机制冒泡到最顶层元素,然后再由 dispatchEvent统一去处理。
对于合成的理解
-
对原生事件的封装
-
对原生事件的升级和改造
例如:当我们给input声明个onChange事件,看下 react帮我们做了什么?
react不只是注册了一个onChange事件,还注册了很多其他事件。
当我们向文本框输入内容的时候,是可以实时的得到内容的。
然而原生只注册一个onchange的话,需要在失去焦点的时候才能触发这个事件,所以这个原生事件的缺陷react也帮我们弥补了。
-
浏览器事件的兼容处理
react在给document注册事件的时候也对兼容性做了处理,比如区分ie浏览器;
4.4 如果一个节点上同时绑定了合成和原生事件,那么禁止冒泡后执行关系是怎样的呢?
浏览器事件的执行需要经过三个阶段,捕获阶段-目标元素阶段-冒泡阶段。
节点上的原生事件的执行是在目标阶段,然而合成事件的执行是在冒泡阶段,所以原生事件会先合成事件执行,然后再往父节点冒泡。
既然原生都阻止冒泡了,那合成还执行个啥嘞。
好,轮到合成的被阻止冒泡了,那原生会执行吗?当然会了。
因为原生的事件先于合成的执行,所以合成事件内阻止的只是合成的事件冒泡。
结论:
原生事件(阻止冒泡)会阻止合成事件的执行
合成事件(阻止冒泡)不会阻止原生事件的执行
两者最好不要混合使用
4.5 受控组件/非受控组件
受控组件
简单讲,就是让表单元素的值跟 React 组件的state绑得死死的,完全同步。用户在表单上不管是打字还是选东西,都像触动了个开关,马上让组件状态更新,接着组件就重新渲染一遍,保证表单显示的值和组件里存的值一模一样。
-
啥时候用它 需要实时查错、给反馈的时候 :像用户注册、登录,还有提交各种信息的表单,对数据准不准要求可高了。就说电商平台的注册表单吧,用户填电子邮箱,刚输完,组件就能立马用正则表达式瞅瞅格式对不对。要是不对,马上在输入框旁边亮个红灯,给个错误提示,还能拦住表单不让提交。这靠的就是受控组件和状态的紧密配合,在onChange事件回调里,实时更新状态,再根据新状态决定显示啥反馈,这么一来,用户就能及时改错误,表单填得又快又准。
-
要让界面联动起来的时候
想象一下在线文档编辑工具,用户调字体大小,文本输入框里的字得跟着变吧,不光如此,标题栏字体、预览区字体,还有排版样式,好多相关的 UI 元素都得一起变。受控组件在这儿就起大作用了,把字体大小这些关键信息放在组件状态里管着。onChange事件一触发,就统一更新状态、重新渲染 UI,各个相关部分就像配合默契的齿轮,一起转起来,给用户的操作提供顺滑流畅的反馈。
非受控组件
表单元素的值由 DOM 自己管着,React 组件平时不插手,就等关键时候,用ref这个 “工具” 去拿 DOM 元素的值。它不折腾那些复杂的状态同步,尊重 DOM 本来的样子,和表单元素打交道简单直接,就像是给咱开了条捷径。
-
适合啥情况 集成第三方表单的时候 :现在前端的工具、库特别多,有些第三方表单库功能超强。要是引入react-bootstrap表单组件库里的高级日期选择器,硬要把它改成受控组件,那麻烦可就大了,不光可能碰到兼容性问题,代码还会变得超级复杂。这时候非受控组件就好用了,用ref轻松拿到日期选择器选的值,顺顺利利就把它集成到 React 项目里,就像搭了座桥,让不同的东西能一起干活。
-
处理文件上传的时候
文件上传这事儿有点特殊,因为浏览器的安全策略,还有文件对象本身的性质,受控组件不好使。就说社交媒体应用里的图片上传功能,用户选了本地照片,文件输入框里的文件对象不能像普通文本一样让 React 状态随便改,更不能直接设置它的值。非受控组件就靠ref精准找到文件输入框,提交的时候一把抓住文件对象,后面就能把文件上传到服务器,这就把文件上传的难题给解决了。
受控组件与非受控组件的对比
(一)数据咋流动的 受控组件:就像建了个双向高速路,数据从用户操作开始,流到组件状态里存着,然后 React 根据状态更新表单元素,又反馈给用户,形成一个闭环,实时响应。每次交互都是状态和视图一起变,保证用户看到的和操作的完全同步。 非受控组件:数据走的是单向路,大多时候在 DOM 里自己流转,表单元素自己管自己的值。React 组件就偶尔用ref去 DOM 那儿取个值,像是偶尔采个蜜的蜜蜂,和 DOM 是种松散关系。
(二)代码复杂不复杂 受控组件:因为要精细维护状态,调度各种复杂逻辑,代码结构就像个精密仪器,一环扣一环。从一开始设状态,到onChange、onBlur等好多事件回调里更新状态,再到根据不同状态显示不同 UI 组件,每个环节都得精心弄。特别是处理多个表单元素联动、深度验证的时候,状态树变得老复杂了,虽然掌控力强,但调试、维护起来不容易,得技术好才能驾驭。 非受控组件:代码风格简单直接,不用搭复杂的状态体系,直接用ref和 DOM 交流,少了中间那些逻辑层。不过在大项目里,要是老用ref在 DOM 和 React 之间穿梭,代码就像散了架,逻辑不连贯,维护起来也麻烦,得看情况用,别给自己挖坑。
(三)啥时候用哪个好 受控组件:要是追求极致的交互体验,对数据管控要求特别精细,那就选它。像金融产品风险评估、医疗信息录入这些重要又复杂的表单,数据准不准至关重要,实时反馈、联动调整都不能少。受控组件靠状态驱动,把用户输入、验证规则、UI 显示绑得紧紧的,虽然复杂,但可靠,能给关键业务护航。 非受控组件:要是碰到特殊情况,像集成第三方表单有兼容性问题,或者处理文件上传这种受限的事儿,它就好使。它不纠结 React 状态,拥抱 DOM 原生力量,用最小代价把功能实现,给项目推进和满足多样需求提供灵活办法。
4.6 渲染列表时,为什么要使用key属性
- 提⾼重排性能:当组件状态更新导致重新渲染时,React会通过key属性快速找到对应的新旧元素并对⽐差异,从⽽避免不必要的DOM操作,提⾼渲染效率;
- 唯⼀标识:在动态数组渲染时,key为每个元素提供了唯⼀标识,帮助React区分各个元素,以便正确地添加、更新或删除元素;
- 优化diff算法:React通过diff算法⽐较新旧虚拟DOM树的差异。key属性使得这⼀过程更加⾼效,因为React可以通过key快速识别出哪些元素是新添加的,哪些需要更新或删除;
- 保持组件状态:当元素具有key属性时,React会尽量复⽤其组件实例,这样即使在列表重新排序时,也可以保持组件的内部状态,如输⼊框中的⽂本内容等;
4.7 如何分配合适的key
- 避免使⽤索引作为key:虽然索引作为key不会引发警告,但它们并不是最佳选择,因为当列表项的顺序发⽣变化时,使⽤索引可能会导致React错误地复⽤组件实例,从⽽引发渲染错误或性能损失;
- 保持key的稳定性:应尽量避免频繁改变元素的key属性值,因为这可能导致组件实例⽆法被正确复⽤,从⽽降低性能;
- 使⽤稳定且唯⼀的值:通常建议使⽤数据中的id或其他唯⼀标识符作为key,这样可以确保key的唯⼀性和稳定性;
- 避免滥⽤key:key属性不应被滥⽤,它应该只在渲染动态列表时使⽤,以确保其有效性和性能优势; 综上所述,key属性在React中扮演着重要⻆⾊,它不仅提⾼了应⽤的性能,还确保了⽤⼾界⾯的正确更新。在实际开发中,合理使⽤key属性可以显著提升React应⽤的效率和⽤⼾体验;
4.8 Hooks的使用原则
只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。
只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。
你可以:
✅ 在 React 的函数组件中调用 Hook
✅ 在自定义 Hook 中调用其他 Hook
4.9 为什么只能在函数最顶层调用 Hook?为什么不要在循环、条件判断或者子函数中调用
hook原理:
初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memorizedState数组(React内部用于存储和管理hooks的数据结构)中。
更新的时候,按照顺序,从 memorizedState中把上次记录的值拿出来。
memorizedState数组是按 hook定义的顺序来放置数据的,如果 hook 顺序变化,memorizedState并不会感知到。
Q:自定义的 Hook 是如何影响使用它的函数组件的?
A:共享同一个 memorizedState,共享同一个顺序。