在network中会标注该请求是在服务器中请求的还是浏览器缓存中的。
一条域名的DNS记录会在本地有两种缓存:浏览器缓存和操作系统(OS)缓存。
1.2.1 浏览器缓存 – 浏览器会缓存DNS记录一段时间。一般是2分钟到30分钟不等。查找浏览器缓存时会按顺序查找: Service Worker–>Memory Cache–>Disk Cache–>Push Cache。
Service Worker:
是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
Memory Cache:
内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
Disk Cache:
存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache。
Push Cache:
Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。
1.2.2系统缓存 – 如果在浏览器缓存里没有找到需要的记录,浏览器会做一个系统调用获得系统缓存中的记录(windows里是gethostbyname)。
1.2.3 路由器缓存** – 接着,前面的查询请求发向路由器,它一般会有自己的DNS缓存。
1.2.4 ISP DNS 缓存** – 接下来要check的就是ISP缓存DNS的服务器。在这一般都能找到相应的缓存记录。
1.2.5 递归搜索** – 你的ISP的DNS服务器从跟域名服务器开始进行递归搜索,从.com顶级域名服务器到Facebook的域名服务器。一般DNS服务器的缓存中会有.com域名服务器中的域名,所以到顶级服务器的匹配过程不是那么必要了。
1.3 DNS域名解析
如果没有访问过该url,就会进行DNS域名解析了。
IP地址和域名一样都是用来做网络标识的,域名和 IP 地址是一一对应的映射关系。
DNS:Domain Name System域名系统(基于RFC规范解释),是万维网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。
DNS解析过程:
1.3.1 用户主机上运行着DNS的客户端,就是我们的PC机或者手机客户端运行着DNS客户端。
1.3.2 浏览器将接收到的url中抽取出域名字段,就是访问的主机名,比如www.feng.com, 并将这个主机名传送给DNS应用的客户端.
1.3.3 DNS客户机端向DNS服务器端发送一份查询报文,报文中包含着要访问的主机名字段(中间包括一些列缓存查询以及分布式DNS集群的工作)。
1.3.4 该DNS客户机最终会收到一份回答报文,其中包含有该主机名对应的IP地址。
1.3.5 一旦该浏览器收到来自DNS的IP地址,就可以向该IP地址定位的HTTP服务器发起TCP连接。
1.4 获取端口号
可能域名下有多个端口号,对应着不同的网络功能,所以在DNS解析之后,浏览器还会获取端口号。
1.5 建立TCP连接
TCP连接,就是耳熟能详的三次握手好朋友,四次挥手是路人。
TCP连接过程:
1.5.1 服务端通过socket,bind和listen准备好接受外来的连接,此时服务端状态为Listen。
1.5.2 客户端通过调用connect来发起主动连接,导致客户端TCP发送一个SYN(同步)字节,告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号,客户端状态为SYN_SENT。
1.5.3 服务器确认(ACK)客户的SYN,并自己也发送一个SYN,它包含服务器将在同一连接中发送数据的初始序列号。
1.5.4 客户端确认服务的ACK和SYN,向服务器发送ACK,客户端状态ESTABLISHED。
1.5.5 服务器接收ACK,服务器状态ESABLISHED。
1.6 HTTP请求
既然我们握手成功了,连接到了Web服务器,浏览器会根据解析到的IP地址和端口号发起HTTP请求。
1.6.1 http协议向服务器发送请求,发送请求的过程中,浏览器会向Web服务器以Stream(流)的形式传输数据,告诉Web服务器要访问服务器里面的哪个Web应用下的Web资源。
1.6.2 服务器接收到浏览器传输的数据后,开始解析接收到的数据,服务器解析请求里面的内容时知道客户端浏览器要访问的是应用里面的哪这个Web资源,然后服务器就去读取这个Web资源里面的内容,将读到的内容再以Stream(流)的形式传输给浏览器。
1.7 关闭TCP
TCP连接中止过程:
1.7.1 某端首先调用close,成为主动关闭端,向另一端发送FIN分节,表示数据发送完毕,此时主动关闭端状态FIN_WAIT_1;
1.7.2 接收到FIN的是被动关闭端,FIN由TCP确认,先向主动关闭端发送ACK,作为一个文件结束符传递给接收端应用进程(放在已排队等候该应用进程接收到的任何其他数据之后),因为FIN的接收意味着接收端应用进程在相应连接无额外数据可接收,接收端状态CLOSE_WAIT;主动关闭端接收到ACK状态变为FIN_WAIT_2;
1.7.3 一段时间后,接收端接收到这个文件结束符的应用进程调用close关闭套接字,向主动关闭端发送FIN,接收端状态为LAST_ACK;
1.7.4 主动关闭端确认FIN,状态变为TIME_WAIT,并向接收端发送ACK,接收端接收到ACK关闭TCP,而主动关闭端一段时间后也关闭TCP;
1.8 浏览器渲染
当浏览器获得一个html文件时,会自上而下的加载,并在加载过程中进行解析渲染。
解析:
1. 浏览器会将HTML解析成一个DOM树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
2. 将CSS解析成 CSS Rule Tree 。
3. 根据DOM树和CSSOM来构造 Rendering Tree。注意:Rendering Tree 渲染树并不等同于 DOM 树,因为一些像 Header 或 display:none 的东西就没必要放在渲染树中了。
4. 有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。下一步操作称之为Layout,顾名思义就是计算出每个节点在屏幕中的位置。
- 再下一步就是绘制,即遍历render树,并使用UI后端层绘制每个节点
渲染:
1. 接收服务器返回html文件。
2. 浏览器开始载入html代码,发现<head>标签内有一个<link>标签引用外部CSS文件,浏览器又发出CSS文件的请求,服务器返回这个CSS文件。
3. 浏览器继续载入html中<body>部分的代码,并且CSS文件已经拿到手了,可以开始渲染页面了。
4. 浏览器在代码中发现一个<img>标签引用了一张图片,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续渲染后面的代码。
5. 服务器返回图片文件,由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码。
6. 浏览器发现了一个包含一行Javascript代码的<script>标签,赶快运行它。
7. Javascript脚本执行了这条语句,它命令浏览器隐藏掉代码中的某个<div> (style.display=”none”)。突然少了这么一个元素,浏览器不得不重新渲染这部分代码。
8. 终于等到了</html>的到来,浏览器泪流满面。
9. 等等,还没完,用户点了一下界面中的“换肤”按钮,Javascript让浏览器换了一下<link>标签的CSS路径。
10. 浏览器召集了在座的各位<div><span><ul><li>们,“大伙儿收拾收拾行李,咱得重新来过……”,浏览器向服务器请求了新的CSS文件,重新渲染页面。
上面已经描述了大概,我们深入的了解一下,了解之后可以考虑考虑我们怎么写代码可以给浏览器减少点工作量。
2.1 解析HTML
HTML的解析是逐行解析。
浏览器的渲染引擎会解析HTML文档并把标签转换成内容树中的DOM节点。
它会解析style元素和外部文件中的样式数据。样式数据和HTML中的显示控制将共同用来创建另一棵树——渲染树。
渲染引擎会尝试尽快的把内容显示出来。它不会等到所有HTML都被解析完才创建并布局渲染树。它会 在处理后续内容的同时把处理过的局部内容先展示出来。
浏览器的解析器通常把工作分给两个组件——分词程序负责把输入切分成合法符号序列,解析程序负责按照句法规则分析文档结构和构建句法树。词法分析器知道如何过滤像空格,换行之类的无关字符。
解析器输出的树是由DOM元素和属性节点组成的。
DOM与标签几乎有着一一对应的关系,如下面的标签
Hello 枫
![](feng.png)
会被转换成如的DOM树:
2.2 解析CSS
CSS选择器的读取顺序是从右向左。
#molly div.haha span{color:#f00}
如上面的代码,浏览器会按照从右向左的顺序去读取选择器。
先找到span然后顺着往上找到class为“haha”的div再找到id为“molly”的元素。
成功匹配到则加入结果集,如果直到根元素html都没有匹配,则不再遍历这条路径,从下一个span开始重复这个过程。
整个过程会形成一条符合规则的索引树,树由上至下的节点是规则中从右向左的一个个选择符匹配的节点。
如果从左向右的顺序读取,在执行到左边的分支后发现没有相对应标签匹配,则会回溯到上一个节点再继续遍历,直到找到或者没有相匹配的标签才结束。
如果有100个甚至1000个分支的时候会消耗很多性能。反之从右向左查找极大的缩小的查找范围从而提高了性能。
这就解释了为什么id选择器大于类选择器,类选择器大于元素选择器。
2.3 解析JS
在浏览器中有一个js解析器的工具,专门用来解析我们的js代码。
当浏览器遇到js代码时,立马召唤“js解析器”出来工作。
解析器会找到js当中的所有变量、函数、参数等等,并且把变量赋值为未定义(undefined)。
把函数取出来成为一个函数块,然后存放到仓库当中。这件事情做完了之后才开始逐行解析代码(由上向下,由左向右),然后再去和仓库进行匹配。
再看一下这段代码
在js预解析的时候,在遇到变量和函数重名的时候,只会保留函数块。在逐行解析代码的时候表达式(+、-、*、/、%、++、–、 参数 ……)会改变仓库里对应的值。
我们来了解一个词“作用域”,现在把这个词拆分一下。
作用:读、写操作
域:空间、范围、区域…
连起来就是能够进行读写操作的一个区域。
“域”:函数、json、……都是作为一块作用域。
全局变量、局部变量、全局函数
一段 也是一块域。在域解析的时候,也是由上向下开始解析。这就解释了为什么引用的外部公共js文件(比如:jquery)应该放到自定义js上边的原因。
再来看一下这段代码
继续跟踪一下解析器的解析过程:首先函数fn()外部的a是一个全局变量,fn()里面的a是一个局部变量。fn()函数同时是一个作用域,只要是作用域,就得做预解析和逐行解析的步骤。所以第一个alert打印的是fn()作用域的仓库指向的变量a,即为undefined。第二个alert打印的是全局的变量a,即为1。
接下来继续看代码,基本雷同的代码,我改变其中一小个地方。
看到这里当解析到fn()的时候,发现里面并没有任何变量,所以也就不往仓库里面存什么,此时的仓库里面是空的,啥也没有。但是这个时候解析并没有结束,而是从函数里面向外开始找,找到全局的变量a。此时打印的正式全局变量a的值。
这里就涉及到一个作用域链的问题。整个解析过程像是一条链子一样。由上向下,由里到外。局部能够读写全局,全局无法读写局部。
来,继续看代码,基本雷同的代码,我再次改变其中一小个地方。
千万不能忘了,在预解析的时候浏览器除了要找变量和函数之外还需要找一些参数,并且赋值为未定义。所以这里的fn(a)相当于fn(var a),这个时候的逻辑就和第一段实例代码一样了。
继续搞事情,继续看代码,基本雷同的代码,我再次改变其中一小个地方。
当代码执行到fn(a);的时候调用的fn()函数并且把全局变量a作为参数传递进去。
此时打印的自然是1,要记住function fn(a)相当于function fn(var a),所以这时候a=2;改变的是局部变量a,并没有影响到全局变量a,所以第二次打印的依然是1。
由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
JavaScript的解释器可以检测到何时程序不再使用一个对象了,当他确定了一个对象是无用的时候,他就知道不再需要这个对象,可以把它所占用的内存释放掉了。
var a = “before”;
var b = “override a”;
var a = b; //重写a
这段代码运行之后,“before”这个字符串失去了引用(之前是被a引用),系统检测到这个事实之后,就会释放该字符串的存储空间以便这些空间可以被再利用。
浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。
3.1 标记清除
这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
当对象,无法从根对象沿着引用遍历到,即不可达(unreachable),进行清除。对于上面的例子,fn() 里面的 a 和 b 在函数执行完毕后,就不能通过外面的上下文进行访问了,所以就可以清除了。
这是当前主流的GC算法,V8里面就是用这种。
不管是高级语言,还是低级语言。内存的管理都是:分配内存使用内存(读或写)释放内存前两步,大家都没有太大异议。关键是释放内存这一步,各种语言都有自己的垃圾回收(garbage collection, 简称GC)机制。
在大部分的应用场景:一个新创建的对象,生命周期通常很短。所以,V8里面,GC处理分为两大类:新生代和老生代。
新生代的堆空间为1M~8M,而且被平分成两份(to-space和from-space),通常一个新创建的对象,内存被分配在新生代。当to-space满的时候,to-space和form-space交换位置(此时,to空,from满),并执行GC。如果一个对象被断定为,未被引用,就清除;有被引用,逃逸次数+1(如果此时逃逸次数为2,就移入老生代,否则移入to-space)。
老生代的堆空间大,GC不适合像新生代那样,用平分成两个space这种空间换时间的方式。老生代的垃圾回收,分两个阶段:标记、清理(有Sweeping和Compacting这两种方式)。
标记,采用3色标记:黑、白、灰。步骤如下:
GC开始,所以对象标记为白色。
根对象标记为黑色,并开始遍历其子节点(引用的对象)。
当前被遍历的节点,标记为灰色,被放入一个叫 marking bitmap 的栈。在栈中,把当前被遍历的节点,标记为黑色,并出栈,同时,把它的子节点(如果有的话)标记为灰色,并压入栈。(大对象比较特殊,这里不展开)
当所有对象被遍历完后,就只剩下黑和白。通过Sweeping或Compacting的方式,清理掉白色,完成GC。
3.2 引用计次
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。
但是用这种方法存在着一个问题,下面来看看代码:
function problem() {
var objA = new Object();
var objB = new Object();
objA.someOtherObject = objB;
objB.anotherObject = objA;
}
在这个例子中,objA和objB通过各自的属性相互引用;也就是说这两个对象的引用次数都是2。在采用引用计数的策略中,由于函数执行之后,这两个对象都离开了作用域,函数执行完成之后,objA和objB还将会继续存在,因为他们的引用次数永远不会是0。这样的相互引用如果说很大量的存在就会导致大量的内存泄露。
大多数浏览器已经放弃了这种回收方式。
如果我问你,浏览器中的缓存有哪些,我相信绝大部分人会说有三种:cookie,sessionStorage,localStorage。
但是诶,我不知为什么大家都叫这三个为缓存,他们叫缓存,我们上面提到的Memory Cache等cache也叫缓存,不是很乱吗,而且浏览器把他们归到了storage里面,storage翻译过来为存储。
学习分享,共勉
题外话,毕竟我工作多年,深知技术改革和创新的方向,Flutter作为跨平台开发技术、Flutter以其美观、快速、高效、开放等优势迅速俘获人心
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
A.someOtherObject = objB;
objB.anotherObject = objA;
}
在这个例子中,objA和objB通过各自的属性相互引用;也就是说这两个对象的引用次数都是2。在采用引用计数的策略中,由于函数执行之后,这两个对象都离开了作用域,函数执行完成之后,objA和objB还将会继续存在,因为他们的引用次数永远不会是0。这样的相互引用如果说很大量的存在就会导致大量的内存泄露。
大多数浏览器已经放弃了这种回收方式。
如果我问你,浏览器中的缓存有哪些,我相信绝大部分人会说有三种:cookie,sessionStorage,localStorage。
但是诶,我不知为什么大家都叫这三个为缓存,他们叫缓存,我们上面提到的Memory Cache等cache也叫缓存,不是很乱吗,而且浏览器把他们归到了storage里面,storage翻译过来为存储。
学习分享,共勉
题外话,毕竟我工作多年,深知技术改革和创新的方向,Flutter作为跨平台开发技术、Flutter以其美观、快速、高效、开放等优势迅速俘获人心
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】