前言
从打开浏览器输入网址到最终的网页呈现在浏览器中,到底经历了哪些过程?下面为大家一一讲解。
总体分为以下几个过程:
- DNS解析——解析域名,获取对应的ip地址
- TCP连接——TCP三次握手
- 浏览器发送http请求
- 服务器处理请求并返回http报文
- 浏览器解析返回的数据并渲染页面
- 断开连接:TCP四次挥手
正文
DNS解析
什么是URL
在讲解DNS解析前,先简单介绍下URL。所谓的URL(Uniform Resource Locator),中文名统一资源定位符,用于定位互联网上的资源。简单来说,就是人们常说的网址。
一个标准的URL格式遵循一定的语法规则:
scheme://host.domain.port/path/filename
对应部分解释如下:
- scheme——定义因特网服务类型。比如http,ftp,file,https
- host——定义域主机(http默认的域主机是www)
- domain——定义了域名,比如:baidu.com
- port——定义主机上的端口号(http默认端口80)
- path——定义资源在主机上的路径
- filename——定义资源文件名
好啦,当我们在浏览器中输入一个网址的时候,浏览器就会对该网址进行DNS解析还获取其对应的ip地址,而这个ip地址,才是资源实际存在的地址。
什么是ip地址
ip地址,指的是互联网协议地址。每一个互联网上的主机都会分配有一个ip地址来作为其身份的标识。ip地址是一个32位的二进制数,比如本机的ip地址为127.0.0.1。而域名相当于是对ip地址的一个伪装。因为相对于纯数字的ip地址来说,具有一定语义的域名更容易被人所理解和记住。而计算机更喜欢纯数字的ip地址,所以为了同时满足以上两者的需求,DNS服务应运而生。
什么是域名解析
DNS协议童工通过域名查找ip地址或是逆向地从ip地址查找域名的服务。DNS是一个网络服务器,简单来说上面存放了各个域名和其ip地址之间的关系数据。
比如:baidu.com 220.114.23.56
域名解析过程
- 浏览器缓存——浏览器会按照一定的频率缓存DNS记录
- 操作系统缓存——如果浏览器缓存中没有记录,会到本地操作系统缓存中查询。(hosts文件)
- 路由缓存——路由器也有DNS缓存
- ISP的DNS服务器:ISP(Internet Service Provider),是互联网服务提供商的简称。ISP有专门的DNS服务器对应DNS查询请求。
- 根服务器:ISP的DNS服务器如果还找不到的话,ISP会代替用户想根服务器发起查询请求,进行查询。根域名服务器返回.com域名服务器的ip地址,然后访问.com域名服务器获取baidu.com域名服务器的ip地址,最后在访问baidu.com域名服务器获取www.baidu.com的ip地址,至此DNS域名解析完成。
TCP三次握手
TCP,一种传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。
网络七层模型
应用层(ftp,http,smtp,pop3)——表示层——会话层——传输层(TCP)——网络层(IP)——数据链路层——物理层
TCP三次握手
- 客户端发送有待带有SYN=1,Seq=x的syn包到服务器,并进入到SYN_SEND状态,等待服务器确认。
- 服务器收到syn包后,必须确认客户的SYN(ack=x+1),同时发送一个自己的SYN(seq=k)包给客户端(即SYN+ACK包),同时服务器进入到SYN_RECV状态。
- 客户端收到服务器的SYN+ACK包后,向服务器发一个确认包ACK(k+1),这些包发送完毕后,客户端和服务器同时进入到ESTABLISHED状态,完成三次握手。
注:tcp在握手过程中并不携带任何数据,而是在三次握手完成之后,才会进行数据的传递。
为什么需要三次握手?
《计算机网络》中讲“三次握手”的目的是“为了防止已经失效的连接请求报文突然又传送到了服务端,因而产生了错误”。
可以这样理解,客户端发起了一个连接请求,但是因为一些原因请求并未及时传递到服务器。而客户端因为长时间收不到服务器的就认为本地连接请求失败,然后就去做别的事了。而过了一段时间,之前的请求到达了服务器,服务器收到请求进行一系列操作后返回给客户端并等待客户端的响应。而此时客户端已经去做别的事情了,根本不会对本次响应做出回答,而服务器那边就一直等啊等,等啊等.......这样就造成了服务器端的资源浪费,所以我们需要三次而不是两次。这样服务器如果在一定时间内没收到客户端的回答,那么就放弃等待,自己也去干别的事了。
那为什么不是四次而是三次呢?
因为服务器的ACK和SYN包可以一起发送啊,并不会产生什么不好的影响,何乐而不为呢?
发送http请求
在TCP三次握手结束后,客户端就可以向服务端发送http请求报文了。
请求报文包含三部分:请求行,请求头,请求主体。
- 请求行包含请求方法,URL,协议版本
例:POST /chapter17/user.html HTTP/1.1
8中请求方法:get,post,put,delete,patch,head,options,trace - 请求头包含了请求的附加信息,一般以key:value的形式存在。比如关于客户端的信息,host(主机名)。
- 请求体包含了多个请求参数的数据,包含了回车符、换行符、请求数据,并不是所有的请求都带有请求数据。
例:name=tom&age=23 (这里承载着name,age三个请求参数)
服务器收到请求后处理请求,并返回响应报文数据。
响应报文包含三部分:响应行,响应头,响应主题。
- 响应行包含了协议版本,状态码,状态码描述
- 响应头包含了一些附加的响应信息
- 相应主体包含回车符、换行符和响应返回的数据,并不是所有的响应都有响应数据
浏览器解析并渲染页面
浏览器在接收到服务器返回的HTML文件后,会对其进行HTML解析。
HTML解析是浏览器的HTML解析器把HTML解析成DOM TREE。在解析否过程中,浏览器会根据HTML文件的结构从上到下解析HTML,HTML元素以深度优先的方式进行解析,而script、link、style等标签会是解析过程产生阻塞,阻塞的情况有:
- 外部样式会阻塞内部脚本的执行
- 外部样式和外部脚本可以并行加载,但是外部样式会阻塞外部脚本执行
- 如果外部脚本带有async属性,则外部脚本的加载与执行不受外部样式影响
- 如果link标签是动态创建生成,不管有无async属性,都不会阻塞外部脚本的加载和执行。
在DOM树和CSS规则树解析完成生成渲染树后,浏览器会进入绘制阶段。调用浏览器的呈现器的“paint”方法,将内容呈现在屏幕上。
回流:当某个元素的尺寸大小或是位置信息发生改变的时候,会触发回流,对元素的大小和位置进行重新计算。
重绘:当某个元素的背景颜色,文字颜色或是其他不影响周围或内部布局的属性发生改变时会触发重绘。
注:回流一定会包含着重绘,而重绘不一定会包含回流。
(插楼)
在实际情况中,DOM操作的代价是非常高的,而页面渲染的瓶颈也都集成中DOM操作上。其中,回流和重绘在DOM操作过程中是对性能影响最大的。所以,我们应该尽可能的避免不必要的回流和重绘操作。
会触发回流和重绘的DOM操作:
- 增加、删除和修改可见DOM元素
- 页面初始化渲染
- 移动DOM元素
- 修改CSS样式,改变DOM元素的尺寸
- DOM元素的内容改变,使得尺寸被撑大
- 浏览器窗口尺寸改变
- 浏览器窗口滚动
可以看出,以上操作是DOM中比较常见的。现代浏览器会针对重排和重绘做性能优化,如把DOM操作积累一批后统一做一次重排或重绘。但是在有些情况下,浏览器会立刻重排或重绘。例如:请求下面的DOM元素布局信息:offsetTop/Left/Width/Height、scrollTop\Left\Width\Height、clientTop\Left\Width\Height、getComputedStyle()或currentStyle。因为这些值都是动态计算的,所以浏览器需要尽快完成页面绘制,然后计算返回值,从而打乱了重排或重绘的优化。
所以针对DOM的优化,可以遵循以下几条实践方法:
合并多次的DOM操作为单词的DOM操作
比如对DOM元素的多个css属性进行修改的时候,可以预先设定好css类,然后通过改变样式名统一修改DOM样式。把DOM元素隐藏或离线后修改
把DOM元素从页面流中脱离或隐藏,然后在修改,这样不会触发重排或重绘。这种方式适合大批量地修改DOM元素,具体有一下3中方式:
(1)使用文档片段
文档片段是一个轻量级的document对象,并不会和特定的页面关联。通过在文档片段上进行DOM操作,可以降低DOM操作对页面性能的影响。这种方式是创建一个文档片段,并在此片段上进行必要的DOM操作,操作完成后将它附加在页面中。
var fragment = document.createDocumentFragment ();
//大量基于fragment的DOM操作
//.......
document.getElementById('app').appendChild(fragment);
(2)设置DOM元素的display样式为none来隐藏元素
这种方式通过隐藏页面的DOM元素,达到在页面中移除元素的效果,经过大量的DOM操作后恢复原来的display样式。只有在一开始隐藏和最后显示的时候会触发重排和重绘。
var ele = document.getElementById('app');
ele.style.display = 'none';
//大量基于fragment的DOM操作
//.......
ele.style.display = 'block';
(3)克隆DOM元素到内存中
这种方式把页面中的DOM元素克隆一份到内存中,然后在内存中对该对象进行操作,完成后再替换页面中的元素。这一也只有在最后一步才会影响页面。
var old = document.getElementById('app');
var clone = old.cloneNode(true);
//大量基于fragment的DOM操作
//.......
old.parentNode.replaceChild(clone, old);
设置具有动画效果的DOM元素的position属性为fixed或absolute
把页面找那个具有动画效果的元素设置为绝对定位,使得元素脱离页面布局流。从而避免了页面繁琐的重排,只涉及到动画元素自身的重排。这种做法可以提高动画效果的展示性能。如果把动画元素设置为绝对定位并不符合涉及的要求,则可以在动画开始时将其设置为绝对定位,等动画结束后再回复原来的设置。谨慎取得DOM元素的布局信息
前面说过,获取DOM的布局信息会有性能的损耗,如果重复的调用,最佳的做法是将这些值缓存在局部变量中。使用时间托管方式绑定事件
在DOM元素上绑定事件会影响页面性能,一方面,绑定事件本身会占用处理时间,另一方面,浏览器保存时间绑定,绑定事件也会占用内存。页面中元素绑定的事件越多,占用的处理时间和内存越大,性能也就相对越差,因此,在页面中绑定事件越少越好。以优雅的处理方式是使用时间托管方式,即利用时间冒泡机制,只在父元素上绑定事件处理,用于处理所有子元素的事件,在事件处理函数中传入参数判断时间源元素,针对不同的源元素做不同处理。
document.getElementById('list').addEventListener('click', (e) => {
if(e.target && e.target.nodeName.toUpperCase == 'LI') {
//针对子元素的处理
//......
}
})
断开连接——四次挥手
在数据传输完毕后,需要断开tcp连接,此时会发起tcp四次挥手。
- 浏览器向服务器发送报文(Fin=1,Ack=z,Seq=x),表示客户端请求报文已经发送完了,准备关闭了。并进入到FIN_WAIT_1状态。
- 服务器收到客户端的断开请求后,发送确认报文(Ack=x+1,Seq=z),表示统一关闭。此时主机进入FIN_WAIT_2状态。
- 服务器在发送完数据以后,也会向客户端发送断开连接的报文(Fin=1,Ack=x,Seq=y),表示我没有响应数据要传了,准备关闭了。此时进入到LAST_ACK状态
- 客户端收到服务器的关闭请求后,会发送一个确认报文(Ack=y+1,Seq=x),表示同意关闭。服务器收到客户单的确认报文后关闭连接。而浏览器在等待一段时间后未收到回复,则正常关闭。