前端爬坡中

66 阅读41分钟

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-cacheno-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 通过加密和身份验证机制提供了更高的安全性;

主要区别对比

特性HTTPHTTPS
安全性无加密,明文传输加密传输,防止窃听和篡改
身份验证通过 SSL/TLS 证书验证服务器身份
默认端口80443
性能较高较低(但现代优化已减少开销)
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 分为两种请求:简单请求和非简单请求。

简单请求

  • GETPOSTHEAD 请求
  • Content-Type 的类型: application/x-www-form-urlencodedmultipart/form-datatext/plain
  • HTTP 请求头:AcceptAccept-LanguageContent-LanguageDPRDownlinkSave-DataViewport-WidthWidth

非简单请求

  • 除了 GETPOSTHEAD 请求以外的其他请求
  • 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 服务器

    • 如果本地缓存中没有,浏览器会向本地 DNS 服务器(通常由 ISP 提供)发送查询请求。
    • 本地 DNS 服务器会检查自己的缓存,如果找到,返回结果。
  • 迭代查询

    1. 根域名服务器:本地 DNS 服务器向根域名服务器(Root DNS Server)查询 .com 的顶级域名服务器地址。
    2. 顶级域名服务器:根域名服务器返回 .com 的顶级域名服务器地址,本地 DNS 服务器向 .com 服务器查询 example.com 的权威域名服务器地址。
    3. 权威域名服务器:顶级域名服务器返回 example.com 的权威域名服务器地址,本地 DNS 服务器向权威域名服务器查询 www.example.com 的 IP 地址。
    4. 返回结果:权威域名服务器返回 www.example.com 的 IP 地址。
  • 返回结果

    • 本地 DNS 服务器将查询到的 IP 地址返回给浏览器,并缓存结果。
    • 浏览器将 IP 地址缓存,并向该 IP 地址发送 HTTP 请求。

总结一下

  1. 浏览器缓存 → 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 和 widthflex-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):
      • 在标记和清除阶段,垃圾回收器需要暂停程序的执行(称为“全停顿”)。
      • 对于大型应用,可能会导致明显的性能问题。
    • 内存碎片
      • 清除阶段会释放不连续的内存块,可能导致内存碎片
      • 内存碎片会降低内存分配效率。

空间碎片化: 所谓空间碎片化就是由于当前所回收的垃圾对象,在地址上面是不连续的,由于这种不连续造成了在回收之后分散在各个角落,造成后续使用的问题;

3️⃣ 标记整理法(Mark-and-Compact)

基于标记清除法,第1阶段(标记)一致,区别在第2阶段;

  • 标记阶段(Mark)

    • 从根对象(如全局对象、当前执行上下文中的变量等)开始,递归遍历所有可达的对象,并标记它们为“活动的”。
  • 整理阶段(Compact)

    • 将所有存活的对象移动到内存的一端,使它们连续排列。
    • 移动过程中,更新对象的引用地址。
  • 清除阶段(Sweep)

    • 清除未被标记的对象,释放它们占用的内存。

优缺点

  • 优点

    • 减少内存碎片:

      • 通过整理内存,使存活对象连续排列,减少内存碎片。
    • 提高内存分配效率:

      • 连续的内存空间可以更快地分配新对象。
    • 适合老生代回收:

      • 老生代中的对象生命周期较长,内存碎片问题更明显,标记整理法可以有效解决这一问题。
  • 缺点

    • 性能开销:

      • 整理阶段需要移动对象并更新引用地址,增加了额外的开销。
    • 暂停时间(Stop-the-World):

      • 在标记和整理阶段,垃圾回收器需要暂停程序的执行,可能导致性能问题。

4️⃣ 分代回收(Generational Collection)

基于对象的生命周期特点,将内存划分为不同的“代”(Generations),并对不同代采用不同的垃圾回收算法。

分代回收的工作流程

新生代(Young Generation)

  • 特点

    • 存放新创建的对象。
    • 大多数对象很快就不再被使用。
  • 回收算法

    • 通常使用 Scavenge 算法(一种复制算法)。

    1. 将新生代内存划分为两个区域:From 空间To 空间

    2. 新创建的对象首先分配到 From 空间。

    3. 当 From 空间满时,触发垃圾回收:

      • 将 From 空间中存活的对象复制到 To 空间。
      • 清空 From 空间。
    4. 交换 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,共享同一个顺序。

4.10 生命周期

5 Vue

5.1 vue2 和 vue3的区别