浏览器方面知识点
浏览器缓存
强缓存
缓存作用分为两种情况,一种是不需要发送http请求,一种是需要发送http请求。 在检查强缓存这个阶段是不需要发送http请求的。 需要通过相应的字段来检查强缓存。
这个字段在http/1.0版本的时候使用的是expires,在http/1.1版本的时候用的cache-control,那为什么会变化呢。
expries
这是一个存在于服务器端响应头当中的一个字段,代表的就是过期时间,告诉浏览器在这个过期时间之前可以直接从缓存里面获取数据,无需再次请求。但是这样会有一个问题,那就是如果服务器和浏览器的时间不一致,则会导致服务器返回的这个时间不准确。
cache-control
这也是一个存在于服务端响应头当中的一个字段,它和expires的区别在于它不是使用具体的过期时间,而是采用过期时长来控制缓存,对应的字段是max-age。 还有一些别的字段:
- pubilc:允许任何缓存缓存资源。
- private:只能允许浏览器端缓存,中间的代理服务器不能缓存。
- no-cache:跳过当前缓存,直接进入协商缓存阶段。
- no-store:不进行任何形式的缓存。
- s-maxage:这是针对代理服务器的缓存时间
当强缓存失效了的时候,也就是不在expires设置的时间之前,或者超过了cache-control设置的缓存时间之后,就进入了第二个屏障,协商缓存了。
Expires 存在时间同步问题(依赖服务器和客户端时间一致),而 Cache-Control: max-age 使用的是相对时间,不受时钟偏差影响,因此更可靠。虽然 Expires 依然可用,但 Cache-Control 更灵活、优先级更高,成为现代缓存控制的首选。
协商缓存
当强缓存失效之后,浏览器会在请求头携带相应的缓存tag字段,由服务器来根据这个tag,来决定是否使用缓存,这就是协商缓存。 这样的缓存分为两种:
Last-Modified
代表最后修改时间。浏览器第一次发送请求的时候,服务器会在响应头中带上Last-modified这个字段,之后浏览器再次发送请求的时候会在请求头中携带If-Modified-Since这个字段,这个字段的值代表的就是服务器传来的最后修改时间。
当服务器接收到If-Modified-Since这个字段之后,会和这个服务器中该资源的最后修改时间去做一个对比,如果请求头中的这个值小于最后修改时间,说明该更新了,会返回新的资源,和常规请求是一样的,否则的话就是返回304,告诉浏览器使用缓存就可以。
Etag
Etag是服务根据文件内容给这个文件生成的唯一标识,当文件内容修改的时候就会改变, 服务器通过响应头将这个值给浏览器。当浏览器接收到这个值之后,会在下一次请求的请求头中将这个值作为If-None-Match这个字段的内容,发送给服务器,当服务器接收到这个字段的值之后会和服务器上该资源的Etag做对比,如果改变了,则证明需要更新,返回新的资源,如果没有改变,则返回304,告诉浏览器使用缓存。
这两者在精准度上Etag优于Last-Modified,Last-Modified 的精度受限于服务器文件系统的时间戳粒度,通常为秒级,因此在高并发或频繁修改的场景下可能无法感知到短时间内的变更。
在性能上Last-Modified要优于Etag,因为Etag要生成一个哈希值,而另一个只需要记录一个时间节点。但是如果两者方式都支持会优先考虑Etag。
缓存位置
当强缓存命中或者协商缓存的时候服务器返回304让我们去直接使用缓存,那这个资源到底是缓存在哪里呢。 按优先级排序分别为:
- Service Worker
Service Worker借助了Web Worker的思路,让JS运行在主线程之外,它脱离了浏览器的窗体,无法直接访问DOM。它可以帮助我们完成很多功能,比如离线缓存,消息推送和网络代理,其中离线缓存就是Service Worker Cache。
它是一个用 JavaScript 编写的可编程网络代理,它运行在浏览器后台,独立于网页主线程。它能够拦截和处理页面发出的网络请求(fetch 事件),并管理缓存,从而实现离线访问、消息推送、后台同步等功能。
Service Worker 是一个革命性的 Web API。它将 Web 应用从被动的、依赖网络的“页面”,转变为能够主动控制网络、管理缓存、在后台运行的“应用”。它是实现离线体验、推送通知、后台同步等高级功能的基石,是构建现代 PWA 不可或缺的核心技术。掌握 Service Worker,就掌握了打造高性能、高留存、类原生体验 Web 应用的关键。
特点是:
独立运行:不运行在页面的主线程之中,不会阻塞页面UI和影响页面交互。
生命周期独立:与页面的生命周期解耦。
核心功能和能力:管理缓存,拦截和处理网络请求,实现离线功能,后台同步,推送通知,周期性同步。
- Memory Cache
Memory Cache是指内存缓存,它效率是最快的,直接在内存中读取缓存,但是存活时间又是最短的,当渲染进程结束之后就不存在了。比较大的JS,CSS是存在Disk Cache里面,小一点的就存在内存中。
- Disk Cache
Disk Cache是存储在磁盘中的缓存,从存取效率上讲是比内存缓存要慢的,但是优势是存储空间大,存储时间长,如果内存的占用率高则会优先存进磁盘空间。
- Push Cache
推送缓存,这是http/2中的内容。Push Cache是现代浏览器内部实现的一个临时、会话级别的缓存机制,是为了优化Server Push 而设计的一个临时存储区域。它的核心作用是拦截对已经被推送的资源的请求,直接使用推送过来的内容,从而减少网络延迟。
由于 HTTP/2 Server Push 在实践中存在缓存控制复杂、资源优先级难以管理、可能导致资源重复传输等问题,主流浏览器(如 Chrome 80+)已逐步移除支持。因此,Push Cache 在现代 Web 开发中已不推荐使用,开发者更倾向于使用 rel=preload 等客户端主动预加载机制。
开发者现在更倾向于使用Link: rel=preload 等客户端控制的预加载技术。
CDN缓存
CDN缓存是通过在全球部署把边缘节点,将静态资源缓存到离用户更近的位置。当用户请求资源的时候CDN先从最近的节点返回内容,减少延迟和源站压力,缓存策略由cache-control等http头部字段控制,支持刷新和与预热。
总结
通过Cache-Contorl验证强缓存是否可以使用,如果不可以则进行协商缓存,即发送http请求,服务器通过If-Modified-Since和If-None-Match字段来判断是更新资源还是返回304,如果更新资源则返回200并且返回新的资源,否则返回304告诉浏览器直接使用缓存。
浏览器的本地存储
浏览器的本地存储由三种Cookie,webStorage和IndexDB,其中webStorage又分为两种,sessionStorage和localStorage。
Cooike
cookie本来不是为了做本地存储的,是为了弥补http在状态管理上的不足。 原先http是无状态的协议,客户端向服务器发送请求,服务器响应,然后就结束了,这就造成了一个问题,下一次请求服务器还是不知道谁是谁,所以就产生了cookie。
本质就是在浏览器当中存储一个很小的文本文件,内部是键值对的方式来存储的。向同一个域名下发送请求都会带上相同的cookie,服务器解析cookie就会知道客户端的状态。 cookie就是用来做状态存储的,但是也有一些缺点,容量很小只有4kb,并且不管需不需要都会带上这个cookie,会带来性能的浪费,并且cookie是以文本的形式传递的,很容易被截取和篡改,很危险。
localStorage
在相同的域名下会存储相同的一段loaclStorage,容量相对于cookie有很大的提升为5M,只存在于客户端,默认不参与服务端的通信,避免了cookie带来的性能问题和安全问题,如果不手动清除浏览器缓存会一直存在。利用localStorage的较大容量和持久特性,可以利用localStorage存储一些内容稳定的资源,比如官网的logo,存储Base64格式的图片资源,因此利用localStorage
sessionStorage
和localStorage大部分都相等,但是有一个本质的区别,数据的生命周期不同,localStorage会一直存在,要手动清除,而sessionStorage 会话级存储:数据只在当前浏览器标签页或窗口有效,关闭之后就会清除。
- 跨标签页共享:同一浏览器的所有同源标签页共享相同的 localStorage
- 独立于标签页:每个标签页/窗口有自己独立的 sessionStorage
IndexDB
这是运行在浏览器中的非关系型数据库,容量非常大,一些特性需要注意:
- 键值对存储。内部采用
对象仓库存放数据,在这个对象仓库中数据采用键值对的方式来存储。 - 异步操作。数据库的读写属于 I/O 操作, 浏览器中对异步 I/O 提供了支持。
- 受同源策略限制,即无法访问跨域的数据库。
总结
浏览器中各种本地存储和缓存技术的发展,给前端应用带来了大量的机会,PWA 也正是依托了这些优秀的存储方案才得以发展起来。
PWA 是 Web 技术的未来发展方向之一。它巧妙地弥合了网页和原生应用之间的鸿沟,兼具两者的优点:拥有网页的易传播、易发现、低门槛特性,同时又提供了原生应用的可靠性、速度和沉浸感。
从输入URL到页面呈现发生了什么?
这是一个非常常考也涉及到很多知识点的问题,要理清楚。
浏览器会根据用户输入的信息判断是搜索还是网址,如果是搜索内容,就将搜索内容+默认搜索引擎合成新的URL;如果用户输入的内容符合URL规则,浏览器就会根据URL协议,在这段内容上加上协议合成合法的URL 比如输入www.baidu.com 地址栏会根据规则,把这段内容加上协议,合成完整的URL,如www.baidu.com
网络请求
现在开始,你已经在浏览器输入一个网址,浏览器会构建请求行
请求行
请求行中包含请求方式:
- GET:请求获取资源
- POST:提交数据到服务器
- PUT:替换或者更新目标资源
- DELETE: 请求删除指定的资源
- PATCH:对资源进行部分更新
- CONNECT:建立连接隧道,用于代理服务器
- OPTIONS: 列出可对资源实行的请求方法,用来跨域请求
- TRACK: 追踪请求-响应的传输路径
GET和POST的区别
在用途上:GET用来向服务器请求数据,例如获取网页,查询信息;POST用来向服务器提交数据,通常用来创建新资源、更新现有资源,例如提交表单、上传文件。
在数据传输位置上:GET请求参数附加在URL的之后所以可见,以查询字符串的形式出现;POST的数据包含在请求体里面,不会显示在URL上,虽然一般不可见,但仍然需要使用HTTPS来进行加密。
数据长度限制:受到URL长度的限制,因此不是传输大量数据;POST请求在数据在请求体当中,理论上没有长度限制,适合用来传输大量数据。
幂等性:GET是幂等的,对同一资源多次GET请求返回结果,且不会改变服务器状态;POST不是幂等的,多次相同的请求可能会创建多个资源,或者产生多次副作用。
可缓存性:GET请求是可缓存的浏览器和代理服务器可以缓存响应结果,以提高性能;POST默认不可缓存。每次都需要发送到服务器进行处理。
满足以下条件的解释简单请求:
请求方法为:GET,POST,HEAD。
请求方法只能包含:
-
Accept
-
Accept-Language
-
Content-Language
-
Content-Type:text/plain
-
multipart/form-data
-
application/x-www-form-urlencoded
并且没有使用ReadableStream等特殊API,如果不满足上面的要求就是复杂请求:浏览器会先发送一个OPTIONS预检请求,询问服务器是否允许该跨域请求。
第二部分为请求地址,通常是URL中的路径部分,也可能包含查询字符串, 第三部分为http的协议版本。
先强缓存在DNS解析
先进行强缓存,如果直接命中则使用缓存,没有的话进行协商缓存,若请求的是域名且无缓存,则先将域名解析成ip地址,需要用到DNS(域名系统),解析的过程就叫DNS解析。
浏览器提供了DNS的数据缓存功能,如果你已经解析过这个域名,会将解析的结果缓存下来,下次遇到就直接使用缓存的,就不用解析。另外,不指定端口,就默认使用80端口。
TCP连接
Chome浏览器对于同一个域名下的tcp连接最多只会允许六个TCP连接,如果大于则剩下的会进入等待转台,如果小于则会直接建立TCP连接。
三次握手
三次握手就是建立一个TCP连接的时候,客户端和服务端总共发送三个包的过程。进行三次握手的主要作用就是为了确认客户端和服务端的发送能力和接收能力是否正常,指定自己的初始化序列号为之后的可靠性传输做准备。实际上就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口的大小。
刚开始客户端处于closed状态,服务端处于listen状态。 进行三次握手: 第一次握手:客户端给服务器发送一个SYN报文,并指明客户端的初始序列号为ISN(c),首部的同步位为SYN=1,SYN为1的不能携带数据,但是要消耗一个序列号,初始序号为seq=x,此时客户端处于SYN_SEND状态。
第二次握手:服务器收到客户端的SYN报文之后,会将自己的SYN报文作为应答,并且也指定了自己的初始化序列号ISN(s),同时把客户端的ISN+1作为ACK的值,表示自己收到了客户端的SYN,此时服务器处于SYN_RCVD状态。 在确认报文段中,SYN=1,ACK=1,确认号为ack=x+1,初始序号为seq=y。
第三次握手:客户端收到SYN报文之后会发送一个ACK报文,把服务器的ISN+1作为ACK的值,表示收到了SYN报文,服务器收到ACK报文之后,双方进入ESTABLISHED状态,这时已经建立起连接,可以发送数据。 发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)。
发送http请求
现在TCP连接建立完毕,可以开始发送http请求。浏览器发送http请求要求携带以下三种东西:请求行,请求头,请求体。在第一步就构建完了请求行,由请求方法,请求地址和协议版本组成。 请求头,像我们之前所说一些字段,Cache-Control,If-Modified-Since,If-None-Match都可能被放进请求头中当缓存信息,还有一些别的,比如:Connection:keep-alive保持长连接。最后请求体,只有POST方法下才会有请求体,常见的就是表单提交。
网络响应
http到达服务器,服务器对它进行处理,返回响应数据,包含响应行,响应头,响应体。
响应行包括三个,协议版本,响应状态码,状态描述。
响应头:包含了服务器还有返回数据的一些信息,比如服务器生成数据的时间,返回数据的类型,以及即将写入的cookie信息。
响应体则为HTML文档或者JSON数据这类的数据。
响应完成了之后,这时候要判断Connection字段, 如果请求头或响应头中包含Connection: Keep-Alive,表示建立了持久连接,这样TCP连接会一直保持,之后请求统一站点的资源会复用这个连接。
解析算法
完成了网络请求和响应,那接下来如果响应头中的Content-Type是text/html,接下来就是浏览器解析和渲染工作了,主要分为:构建DOM树,样式计算,生成布局树。
构建DOM树
由于浏览器无法解析HTML字符串,所以要将这一系列的字节流转换为更有意义的并且方便操作的数据结构,就是DOM树,本质上是一个以document为根节点的多叉树。 常规的编程语言都是上下文无关的,而html是相反的,不是上下文无关的,比如解析到form标签的时候,会先查看上下文,如果父标签也是form标签则会跳过当前标签。
html的解析算法分为两个阶段: 1.标记化(词法分析) 2.建树(语法分析)
标记化算法
这个算法输入为HTML文本,输出为HTML标记,也成为标记生成器。其中运用有限自动状态机来完成。即在当当前状态下,接收一个或多个字符,就会更新到下一个状态。
建树算法
解析器首先会生成一个document对象,标记生成器会把每一个标记信息发送给建树器,建树器收到相应的标记,会先创建对应的DOM对象,之后做两件事,先将DOM对象放入DOM树中,再将标记压入存放开放元素(与闭合标签意思对应)的栈中。
容错机制
讲到HTML5规范,就不得不说它强大的宽容策略, 容错能力非常强,虽然大家褒贬不一,不过我想作为一名资深的前端工程师,有必要知道HTML Parser在容错方面做了哪些事情。
样式计算
浏览器接收到css文本是无法解析的,需要转化为一个结构化的对象styleSheets,可以通过document.styleSheets,来查看最终的结构。有一些样式不容易被渲染引擎理解,所以需要标准化,像rem,red这种。
样式已经被格式化和标准化之后就可以开始计算每个节点具体的样式信息了。主要就是两个规则:继承和层叠。
继承:每个子节点都会继承父节点的样式属性,如果没有找到则会采用浏览器默认样式UserAgent样式。
层叠:css的最大特点就是层叠性,最终的样式取决于各个元素共同作用下的效果,层叠性的计算流程为:
- 收集所有声明
- 按来源和!important排序
- 按特异性排序
- 按声明顺序排序
不过值得注意的是,在计算完样式之后,所有的样式值会被挂在到window.getComputedStyle当中,也就是可以通过JS来获取计算后的样式,非常方便。
渲染树的构建
浏览器将DOM Tree 和 CSSOM Tree合并成一个渲染树(Render Tree),这棵渲染树只会包含可见的节点,像link,meta,script,还有display:none的元素不会包含在内,而渲染树只包含了对应DOM节点的几何信息和最终计算的样式信息,但没有包含布局信息。
所以我们需要重新遍历渲染树,计算每一个可见元素在视口中的确切位置,生成一棵布局树(Layout Tree),布局完成之后每个元素都知道自己该什么位置。
渲染过程
建图层树
我们不仅要考虑样式和位置信息,还需要考虑一些复杂的动画效果,还有当出现层叠上下文的时候如何控制显示和隐藏等。 为了解决这个问题,我们在生成了Layout Tree之后还需要对特定的节点进行分层构建一颗图层树(Layer Tree)。
一般情况下,节点的图层会属于父节点的图层(合成层),什么时候会被提升为一个单独的合成层,一种是显示合成,一种是隐式合成。
显示合成
一、 拥有层叠上下文的节点。
层叠上下文也基本上是有一些特定的CSS属性创建的,一般有以下情况:
- HTML根元素本身就具有层叠上下文。
- 普通元素设置position不为static并且设置了z-index属性,会产生层叠上下文。
- 元素的 opacity 值不是 1
- 元素的 transform 值不是 none
- 元素的 filter 值不是 none
- 元素的 isolation 值是isolate
- will-change指定的属性值为上面任意一个。
will-change 是一个性能提示,告诉浏览器“我即将改变这个属性”,浏览器可能会提前将其提升为独立的合成层以优化性能。但它不一定会创建层叠上下文,除非该元素本身已满足创建条件。
二、需要剪裁的地方。
比如一个div,你只给他设置 100 * 100 像素的大小,而你在里面放了非常多的文字,那么超出的文字部分就需要被剪裁。当然如果出现了滚动条,那么滚动条会被单独提升为一个图层。
隐式合成
简单来说就是一个层级等级低的节点被提升为单独的一个图层,那么所有层级比他高的节点就都会被提升到单独的图层,这就是层爆炸的原理,那为什么需要使用合成层呢,这是为了:
- 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
- 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
- 对于 transform 和 opacity 效果,不会触发 layout 和 paint
生成绘制列表
接下来渲染引擎会将图层的绘制拆分成一个一个绘制的指令,生成一个待绘制列表,相当于给后面的绘制操作做了一波计划。
生成图块和生成位图
其实渲染进程的绘制操作有专门的线程来实现的,这个线程叫做合成线程。绘制列表准备好了之后,渲染进程的主线程会给合成线程发送commit消息,把绘制列表提交给合成线程。
首先视口很小,有时候滑到底部都需要很久,如果直接全部绘制出来很耗费性能,所以第一件事情就是将图块分层。大大加速首屏渲染。 因为浏览器内存上传到GPU很慢,所以首次合成图块的时候只用一个分辨率很低的图片,正常图块内容绘制完了之后在替换,这也是Chorm浏览器优化首屏加载速度的一个优化。
顺便提醒一点,渲染进程中专门维护了一个栅格化线程池,专门负责把图块转换为位图数据。
然后合成线程会选择视口附近的图块,把它交给栅格化线程池生成位图。
生成位图的过程实际上都会使用 GPU 进行加速,生成的位图最后发送给合成线程。
显示器显示内容
栅格化操作完成后,合成线程会生成一个绘制命令,即"DrawQuad",并发送给浏览器进程。
浏览器进程中的viz组件接收到这个命令,根据这个命令,把页面内容绘制到内存,也就是生成了页面,然后把这部分内存发送给显卡。为什么发给显卡呢?我想有必要先聊一聊显示器显示图像的原理。
无论是 PC 显示器还是手机屏幕,都有一个固定的刷新频率,一般是 60 HZ,即 60 帧,也就是一秒更新 60 张图片,一张图片停留的时间约为 16.7 ms。而每次更新的图片都来自显卡的前缓冲区。而显卡接收到浏览器进程传来的页面后,会合成相应的图像,并将图像保存到后缓冲区,然后系统自动将前缓冲区和后缓冲区对换位置,如此循环更新。
看到这里你也就是明白,当某个动画大量占用内存的时候,浏览器生成图像的时候会变慢,图像传送给显卡就会不及时,而显示器还是以不变的频率刷新,因此会出现卡顿,也就是明显的掉帧现象。
跨域问题
跨域是指浏览器为了安全问题,限制了不同源之间的跨域请求问题,这里的'源'是由协议,域名,端口三者共同决定。
解决跨域的方法有哪些:
JSONP
利用 script标签不受同源策略限制的特性,动态创建script标签请求数据。 缺点只支持GET请求。 手写JSONP实现:
function jsonp(url, params = {}, callbackName = 'callback') {
// 将参数转换为url查询字符串
const queryString = Object.keys(params).map(
keys => `${keys}=${params[keys]}`
).join('&')
// 生成唯一的回调函数名
const callbackId = '_jsonp_' + Date.now() + '_' + Math.floor(Math.random() * 1000)
// 构造完整的url
const fullUrl = `${url}?${queryString}&${callbackName}=${callbackId}`
// 创建script标签
const script = document.createElement('script')
script.src = fullUrl
script.async = true // 异步加载
// 在window上挂载回调函数
window[callbackId] = function (data) {
if (typeof params.success === 'function') {
params.success(data)
}
// 清理,删除script和全局函数
document.head.removeChild(script);
delete window[callbackId];
}
document.head.appendChild(script)
}
使用window挂载,删除避免内存泄漏,用户可以通过params.success传入函数,异步加载。
CORS(跨域资源共享)
CORS是W3C标准,通过在服务器端设置HTTP响应头来告诉浏览器允许跨域请求。
(1)简单请求过程:
对于简单请求,浏览器会直接发出CORS请求,它会在请求的头信息中增加一个Orign字段,该字段用来说明本次请求来自哪个源(协议+端口+域名),服务器会根据这个值来决定是否同意这次请求。如果Orign指定的域名不在许可范围之内,服务器会返回一个正常的HTTP回应,浏览器发现没有上面的Access-Control-Allow-Origin头部信息,就知道出错了。这个错误无法通过状态码识别,因为返回的状态码可能是200。
在简单请求中,在服务器内,至少需要设置字段: Access-Control-Allow-Origin
2)非简单请求过程
非简单请求是对服务器有特殊要求的请求,比如请求方法为DELETE或者PUT等。非简单请求的CORS请求会在正式通信之前进行一次HTTP查询请求,称为预检请求。
浏览器会询问服务器,当前所在的网页是否在服务器允许访问的范围内,以及可以使用哪些HTTP请求方式和头信息字段,只有得到肯定的回复,才会进行正式的HTTP请求,否则就会报错。
预检请求使用的请求方法是OPTIONS,表示这个请求是来询问的。他的头信息中的关键字段是Orign,表示请求来自哪个源。除此之外,头信息中还包括两个字段:
- Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法。
- Access-Control-Request-Headers: 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段。
服务器在收到浏览器的预检请求之后,会根据头信息的三个字段来进行判断,如果返回的头信息在中有Access-Control-Allow-Origin这个字段就是允许跨域请求,如果没有,就是不同意这个预检请求,就会报错。
代理服务器(proxy)
利用同源策略只针对浏览器的特点,在前端服务器(如vite server Nginx,webpack-dev-server)上设置代理,将请求转发给目标服务器。
正向代理和反向代理的区别
正向代理:客户端想要获得一个服务器的数据,但是因为一些原因无法直接获取。所有client通过设置代理服务器,指定目标服务器,之后代理服务器向目标服务器转交请求并且将得到的内容发送给客户端,本质上实现了对真实服务器隐藏真实客户端的目的。实现正向代理需要修改客户端,比如浏览器配置。
反向代理:是一种位于前端的代理服务器,接收来自客户端的请求,然后将这些请求转发给后端内部服务器,并将后端的响应结果返回给客户端。对于客户端来说,它就像是直接与目标服务器通信,但实际上通信是通过反向代理完成的。 本质上是实现了对客户端隐藏真实服务器的作用。一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。
WebSocket
websocket协议本身支持跨域,且不受同源策略影响。WebSocket是一基于TCP的全双工通信协议,通过一次http握手升级协议,建立持久连接。允许客户端和服务器在单个TCP连接进行实时数据交换。和传统的http请求-响应不同,Websocket建立连接之后,双方可以主动发送数据,无需轮询。
“WebSocket 是一种基于 TCP 的全双工通信协议,通过一次 HTTP 握手升级协议,建立持久连接。相比轮询,它大大减少了延迟和开销,适用于聊天、实时数据等场景。典型的应用场景在:
实时聊天应用:微信,钉钉。实时数据展示。在线游戏。通知系统。
1.websocket起始于一个http请求,使用upgrade:websocket来升级协议成为websocket。
2.数据通信阶段:连接建立之后,服务器和客户端可以随时互相发送数据帧(frame):
- 数据可以是文本(UTF-8)或二进制(Blob,ArrayBuffer)
- 没有http的请求头开销,通信效率高
- 保持长连接
3.关闭连接
任何一方都可以发送关闭帧(close frame),通知对方关闭连接。
fetch
是一个现代浏览器提供的基于promise的原生网络请求API,用于替代传统的XMLHttpReauest。
具有以下特点:
- 基于promise:,返回一个promise对象,支持链式调用和async/await语法。
- 简洁的API设计:使用fetch(url,options)的形式发起请求,options可以配置请求方法,请求头和请求体等。
- 默认不携带cookie:除非显式设置credentials:'include'。
- 在遇到网络错误的时候reject:HTTP状态码如404或500不会触发catch,要手动检查。
- 支持流式响应:response.body是一个可读流,适合处理大文件或实时数据。
局限性: 不支持取消请求,虽然可以使用AbortController。 默认不带cookie,可以显示设置。 对浏览器的兼容差。
AJAX它开启了现代 Web 应用的大门,是 SPA(单页应用)、前后端分离架构的基石。虽然实现方式从 XMLHttpRequest 演进到 fetch 和各种库,但其核心思想——异步、局部更新——依然是前端开发的核心。
AJAX是SAP技术的核心技术之一。SPA指整个应用只有一个HTML页面,所有页面的切换和内容更新都是通过JavaScript动态加载和渲染来完成的,而不是像传统MPA一样每次跳转都重新加载整个页面。
AJAX是实现SPA的关键技术,SPA是AJAX最典型最重要的应用之一。
axios
Axios 是一个基于 Promise 的 HTTP 客户端,可用于浏览器和 Node.js 环境。它提供了一种简洁、强大的方式来发送 HTTP 请求,是现代 JavaScript 应用(尤其是使用 React, Vue, Angular 等框架时)进行网络通信的流行选择。
Axios的所有方法都返回一个Promise,使得处理异步操作变得强大和自然。可以使用.then或者.catch链式处理成功和失败,或者使用async/await,让代码更像同步代码,使得逻辑更清晰。
好处在于:浏览器器端和客户端兼容。请求和响应拦截器(interceptors)。转换请求和响应数据。可以使用AbortController来取消请求。Axios 默认会将请求体中的 JavaScript 对象自动序列化为 JSON 字符串(Content-Type 设为 application/json)。同样,它也会自动将响应头为 application/json 的响应体解析为 JavaScript 对象。这大大简化了与 RESTful API 的交互。
Axios有非常丰富的配置选项。清晰的错误处理。Axios 将错误分为网络错误、HTTP 状态码错误(如 4xx, 5xx)和请求取消错误等。错误对象通常包含 response(包含服务器响应信息,如状态码、数据)、request(原始请求信息)和 config(请求配置)。这使得错误诊断和处理更加精确。
请求拦截: axios.interceptors.request.use
最常见用途。例如,从本地存储获取 JWT Token 并添加到 Authorization 头。
响应拦截:axios.interceptors.response.use
- Token 刷新:检测到 401 错误时,尝试使用 Refresh Token 获取新的 Access Token,然后重试原始请求(这是实现无感刷新的关键)。
AbortController
AbortController 是一个现代的、标准化的 Web API,用于中止一个或多个 DOM 请求(如 fetch)或底层操作(如 XMLHttpRequest,Axios 在浏览器中就是基于它)。它比 Axios 旧的 CancelToken API 更简洁、更强大,是当前推荐的取消请求方式。
AbortController 接口提供了一个控制器,允许你根据需要中止一个或多个 Web 请求。
-
AbortController构造函数:- 创建一个新的
AbortController实例。 const controller = new AbortController();
- 创建一个新的
-
AbortController.signal属性:- 这是一个
AbortSignal对象实例。 - 这个
signal对象可以被传递给一个或多个需要被控制的异步操作(如fetch或axios请求)。 - 该
signal对象有一个aborted属性,当abort()方法被调用后,aborted的值会变为true。 - 它还支持
addEventListener('abort', ...)来监听中止事件。
- 这是一个
-
AbortController.abort()方法:- 调用此方法会中止与该控制器关联的所有操作。
- 一旦调用
abort(),signal.aborted就会变为true,并且会触发abort事件。 - 调用
abort()后,任何后续对该AbortController的调用(包括再次调用abort())都将被忽略。
实际的应用场景为用户发起一个耗时性的搜索或者文件上传,然后点击取消按钮。
在React组件中发起请求获取数据,之后导航导论其它页面,需要取消请求避免造成内存泄漏和在已经卸载的组件上设置状态。
JS为什么是单线程的
首先了解进程和线程的概念。
进程
CPU分配资源的最小单元。浏览器是多进程多线程的概念,CPU可以有很多进程,采用时间片轮转调度算法来实现同时运行多个进程。浏览器每打开一个网页也相当于是开启一个进程,包含:
Browser进程:
- 浏览器的主进程,负责浏览器界面的显示,与用户的交互,如前进,后退。
- 负责各个页面的管理,创建和销毁其它进程。
- 将渲染进程得到的内存中的位图绘制到用户界面上。
- 网络资源的管理和下载等。
GPU进程: 只有一个用于3D/动画绘制等等。
渲染进程:
- 通常所说的浏览器内核。
- 每个Tab页面都有一个渲染进程,互不影响。
- 主要作用为页面渲染,脚本执行,时间处理等。
网络进程:主要负责网络资源加载。
第三方插件进程:每一个不同的插件对应不同的进程。
进程通常情况下,不同的进程拥有独立的地址空间,不直接共享资源。但是,操作系统提供了进程之间的通信机制(IPC),允许进程在必要的时候共享特定资源:
管道通信:管道就是操作系统在内核开辟一段缓冲区,进程1可以将需要交互的资源拷贝到缓冲区,进程2就可以读取了 匿名管道只允许有亲缘关系的的进程,命名管道无亲缘关系的的进程只要知道名字即可进行通信。
消息队列通信: 消息队列就是一个消息的列表,用户可以在队列中添加和读取消息。消息队列提供了从一个进程向另一个进程发送一个数据块的方法。优点:解耦发送方和接收方,支持多对多的通信。缺点:有消息大小和队列长度的限制。
共享内存通信:映射一段能被其它进程所访问的内存,这段内存由一个进程创建,但多个进程都可以访问。访问速度极快,但是往往会产生多个数据竞争的问题,可以结合信号量,互斥锁等同步机制来使用。
信号量通信:共享内存最大的问题就是多进程竞争内存的问题,可以使用信号量来解决这个问题,信号量本质就是一个计数器,用来实现进程之间的互斥和同步。例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就会访问不了内存1。所以说,信号量也是进程之间的一种通信方式。
信号通信:信号(Signals )是Unix系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。
套接字通信:是网络编程和本地进程间通信的核心抽象。提供了一种统一的接口,使得进程无论是在一台机器上还是在不同的网络主机上,都可以通过相同的编程模型进行通信。
Unix域套接字:它绕过了网络协议栈,用于本地IPC的主要形式,性能远超套接字。
我们最主要的就是对渲染进程的理解:
渲染进程当然也是多线程的,分为:
-
GUI渲染线程- 负责渲染页面,布局和绘制
- 页面需要重绘和回流时,该线程就会执行
- 与js引擎线程互斥,防止渲染结果不可预期
-
JS引擎线程- 负责处理解析和执行javascript脚本程序
- 只有一个JS引擎线程(单线程)
- 与GUI渲染线程互斥,防止渲染结果不可预期
-
事件触发线程- 用来控制事件循环(鼠标点击、setTimeout、ajax等)
- 当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中
-
定时触发器线程- setInterval与setTimeout所在的线程
- 定时任务并不是由JS引擎计时的,是由定时触发线程来计时的
- 计时完毕后,通知事件触发线程
-
异步http请求线程-
浏览器有一个单独的线程用于处理AJAX请求
-
当请求完成时,若有回调函数,通知事件触发线程
-
我们该怎么回答JS为什么是单线程的这个概念呢?
明确"单线程"的含义:首先js的单线程指的就是js引擎在同一时间只能执行一个任务,不能并行处理多个任务。这意味着所有的代码,包括函数的调用、事件处理、DOM操作必须排队等待,不能同时进行。
讲历史背景和设计初衷:原先设计JavaScript是为了为网页添加交互功能,比如表单验证、动态修改DOM、响应用户点击之类的。在那个时代,浏览器的环境相对简单,如果我们允许多线程去操作DOM,会带来复杂的同步任务。比如: 线程A在修改DOM。 线程B在删除DOM。 这会导致状态不一样,有可能导致页面崩溃。为了避免这种复杂性,JS被设计成单线程 + 事件驱动的模型。
事件循环: 虽然js是单线程的,但并不意味着js不能处理异步操作或者并发任务,通过事件循环和回调函数实现了高效的异步操作。 我们可以把js执行的任务分为同步和异步任务,同步任务在主线程中执行,形成调用栈,遇到异步任务会将任务交给对应的Web api 处理,处理完之后会将回调函数放入任务队列中,执行栈中的同步任务执行完之后就会去查看任务队列并执行其中的回调。这种机制使得js能在单线程下处理高并发的用户交互和网络请求,而不会阻塞UI。
JS 引擎线程和 DOM 操作、样式计算、布局(reflow)等操作在主线程中执行,会阻塞 UI 渲染。但现代浏览器通过合成层(composited layers) 将某些动画(如 transform、opacity)交给合成线程处理,从而实现非阻塞式动画。
现代发展Web Worker和多线程支持:Web Worker是用来处理一些CPU密集型任务的,他在后台线程中运行脚本,独立于主线程。但是不能访问DOM,和主线程之间通过postMessage来通信。
总结来说,JavaScript 之所以是单线程的,根本原因在于:
- 简化编程模型:避免多线程带来的锁、死锁、竞态等问题;
- 保障 DOM 操作的安全性;
- 配合事件循环实现高效的异步非阻塞 I/O。
虽然单线程在 CPU 密集型任务上有局限,但通过异步机制和 Web Workers,JavaScript 在保持简单性的同时,也具备了处理复杂应用的能力。
因此,JavaScript 的“单线程”不是缺陷,而是一种精心权衡后的设计选择,非常适合其在浏览器中的角色。
Http和Https
https其实就是 http + SSL/TLS ,在http的基础上加了一层安全传输层,用来实现数据加密传输,核心目标是实现 加密,身份认证,数据完整性。
为了实现这些,https使用了混合加密的方式,结合对称加密和非对称加密的优点。
- 客户端向服务器发起请求,请求中包含使用的协议版本号、生成的一个随机数、以及客户端支持的加密方法。
- 服务器端接收到请求后,确认双方使用的加密方法、并给出服务器的证书、以及一个服务器生成的随机数。
- 客户端确认服务器证书有效后,生成一个新的随机数,并使用数字证书中的公钥,加密这个随机数,然后发给服 务器。并且还会提供一个前面所有内容的 hash 的值,用来供服务器检验。
- 服务器使用自己的私钥,来解密客户端发送过来的随机数。并提供前面所有内容的 hash 值来供客户端检验。
- 客户端和服务器端根据约定的加密方法使用前面的三个随机数,生成对话秘钥,以后的对话过程都使用这个秘钥来加密信息。
使用非对称加密是因为安全性高,但是速度慢,所以用于传输对称密钥,使用对称密钥是因为速度快,适合大量数据加密。
TLS握手的过程:
-
Client Hello: 向服务器发送支持的TLS版本,加密套件,随机数。
-
Server Hello:服务器选择加密套件,返回自己的随机数和数字证书(含有公钥)
-
证书验证:客户端验证证书的有效性
-
密钥交换:客户端生成预主密钥,用服务器发送的公钥加密后发送(RAS密钥交换),双方结合随机数,预主密钥,生成相同的会话密钥。 之后使用对称加密(AES)加密后续通信。
现代 TLS 握手通常使用 ECDHE 算法进行密钥交换。客户端和服务器各自生成临时的椭圆曲线密钥对,交换公钥,然后通过 ECDH 算法计算出共享的预主密钥。这种方式支持前向保密(PFS),即使服务器私钥泄露,也无法解密历史通信。
V8引擎垃圾回收机制
栈内存的回收
栈内存主要用于存储简单数据类型和函数执行上下文,栈内存的回收非常高效,通过调用栈和一个ESP指针来实现垃圾回收。
函数在被调用的时候,它的执行上下文会被压入调用栈,ESP指针会指向这个新的上下文。 函数执行结束的时候,ESP指针会往下移动,指向调用它的函数的执行上下文。
回收发生:指针的下移操作,本质上就标记了上一个函数的执行上下文为无效状态。虽然内存中的数据可能暂时还存在,但它们已经无法被访问。当下一次有新的函数调用时,这块内存会被直接覆盖,从而实现“回收”。
堆内存的回收
堆内存用于存储引用数据类型和闭包等。由于这些数据的生命周期不确定,且有可能被多个地方引用,所以要比栈的垃圾回收复杂的多,采用分代回收的策略:
新生代:存放生命周期短的对象,如函数内部临时创建的变量。
新生代的内存回收:Scavenge算法。
新生代的空间小,垃圾回收频繁。被划分为两个相等的空间,From空间和To空间。From空间表示正在使用的空间,To空间表示空闲的空间。
回收流程: 从根对象开始遍历并标记From空间中存活的对象,将标记为存活的对象复制到To空间,按照顺序排列,避免了碎片。 清空整个From空间,将From和To空间互换,然后准备接收新的对象。
对象晋升:当一个对象在新生代中经历了一次或多次垃圾回收后依然存活,或者复制过程中 To 空间的使用量超过25% ,该对象就会被晋升到老生代。
老生代:存放生命周期长和体积大的对象,如全局对象,经过多次回收仍然存活的对象,发生晋升。
老生代的内存回收-标记清除与标记整理。
标记清除:从根对象出发,遍历整个堆,标记所有可达也就是正在使用的对象,通常采用三色标记法(白,灰,黑)来高效跟踪。 清除所有未被标记的对象,此过程会产生内存碎片。
标记整理 : 标记阶段:从根对象出发,遍历整个堆,标记所有可达也就是正在使用的对象,通常采用三色标记法(白,灰,黑)来高效跟踪。 整理阶段:将所有存活的对象向内存的一端去移动,让它们紧密排列,从而消除内存碎片。 清理:清理整理后剩余的空间。
V8 会根据内存使用情况和碎片程度,在标记-清除和标记-整理之间动态选择。通常先执行标记-清除,当碎片严重时再执行标记-整理。
增量标记 (Incremental Marking) :
- 将耗时的标记过程分解成多个小步骤。
- 每执行一个小步骤,就暂停垃圾回收,让 JavaScript 主线程执行一小段业务代码,然后再继续下一个标记步骤。
- 这大大减少了单次停顿时间,提升了应用的响应速度。