网络请求部分
- 在浏览器输入URL地址
https://www.teambition.com/
- 浏览器构建请求行
GET / HTTP/1.1
请求方式 根路径 协议以及协议版本
HTTP协议相关版本拓展
HTTP/0.9
最初版本,只支持GET请求,且只能请求HTML格式的资源
HTTP/1.0
增加了POST和HEAD请求方式
可以通过Content-Type字段支持请求访问多种数据格式,如text/html, image/jpeg(即MIME type,多用途互联网邮件扩展类型)
支持Cache(缓存),分为强缓存和协商缓存
无状态协议,每次TCP连接只能发送一个请求,不支持keep-alive方式保持连接的状态
HTTP/1.1
引入持久连接,即TCP在建立连接之后默认不关闭,不同于1.0版本时只能发送一个请求,1.1在连接开启的状态下可以发送多个请求
可以通过Connection: close字段来关闭持久连接
引入管道机制,即允许在连接过程中,同时发送多个请求,增加并发性,提高了响应的效率。但响应依旧是按顺序的
新增PUT(将数据从客户端发送至服务器取代指定的内容), PATCH(对已知资源进行局部更新), OPTIONS, DELETE请求
新增Host字段:指明服务器的域名
HTTP队头阻塞问题:HTTP协议是基于请求-应答的模式进行的,也就是说请求发送出去后是需要按顺序进行相应的,所有的HTTP请求都放在了一个任务队列中,当其中存在一个需要长时间响应的请求时,后续的请求就会阻塞住
并发连接:1.1中允许在一个持续的TCP连接中,同时发送多个HTTP请求,chrome允许最多并发6个
域名分片:既然一个域名最多可以发送6个,那在当前域名下再去划分二级或者三级域名,以此来增加并发请求的条数
HTTP/2.0
增加双工模式:客户端可以并发HTTP请求,服务端也可以并发响应HTTP请求,有效的解决了队头阻塞的问题
服务器推送:允许服务器未经请求,即可向客户端推送消息(适合加载静态资源)
- 首先查找强缓存,如果存在,则直接使用,如果不存在,则进入下一步
浏览器缓存
强缓存(不需要发送HTTP请求,保存于客户端的缓存)
1.0:使用Expires(过期时间)字段表示
1.1:使用Cache-Control字段表示(优先级更高)
协商缓存(需要发送HTTP请求,通过携带字段告诉服务器是否需要使用缓存)
Last-Modified(最后修改时间)
ETag(服务器根据当前内容,给文件生成唯一标识)
- DNS解析
由于数据是通过IP地址来进行转发的,但是我们在浏览器上输入的是域名,故我们需要先将域名进行解析得到对应的IP地址,才能进行接下来的一系列转发等操作,域名与IP地址的映射过程就称之为DNS解析,且浏览器提供了DNS数据缓存,如果一个域名已经经过解析,则浏览器会将解析内容缓存下来,下次如果是相同的域名则不再需要重新解析直接调用缓存即可
- 建立TCP连接
将域名解析成IP地址后,就可以开始进行与服务器短的TCP连接了
TCP协议(面向连接,可靠的基于字节流的传输层协议)
三次握手,四次挥手
- 发送HTTP请求
TCP连接建立完成后,就可以开始发送HTTP请求给服务器端了
浏览器发送的HTTP请求包含三个部分:请求行,请求头和请求体
请求行
第一步事已经建立,即
GET / HTTP/1.1
请求头
即诸如缓存字段之类的信息
请求体
POST请求时携带的需要发送给服务器端的数据
- 服务器响应HTTP请求
客户端成功发送一次HTTP请求后,服务器端接收HTTP请求,并对请求做出响应
响应内容同样包含三个部分:响应行,响应头和响应体
响应行
HTTP/1.1 200 OK
http协议以及版本,状态码,状态描述
响应头
同请求头类似
响应体
服务器端返回的相关数据
- 当响应完成之后,需要根据Connection字段来判断是否断开连接或者保持连接状态
- 当连接断开后,请求-响应流程结束,即客户端与服务器端的数据传输流程结束,接下来就需要浏览器根据数据以及html,css,js文件来绘制页面了
页面解析部分
- 构建DOM树
对于浏览器而言是无法直接将HTML文件进行对应标签的渲染的,所以需要先将HTML转化为浏览器可以识别的数据结构,即转化为DOM树,一个以document为根节点的多叉树
标记化算法
<html> <body> hello world </body> </html>
- 遇到<,状态更新为标记打开
- 接收[a-z]字符串,状态为标记名称
- 遇到>,表示标记名称记录完成,状态更新为数据状态
- 读取hello world字符串
- 遇到<,状态更新为标记打开
- 遇到/,创建 end tag
- 接收[a-z]字符串,状态为标记名称
- 遇到>,表示标记名称记录完成,状态更新为数据状态
- 下面的标签依次按照顺序进行读取
建树算法
- DOM树是一个以document为根节点的多叉树,因此,首先初始化,创建document根节点
- 接收标记器传过来的html标签,状态变为before html,同时创建HTMLHtmlElement的DOM元素,添加到document根节点,压栈操作
- 状态变为before head,接收标记器传来的body标签,没有head标签,这时建树器自动创建HTMLHeadElement的DOM元素,进入到in head状态,然后直接跳转到after head状态
- 由于上一步标记器已经传入了body标签,故创建HTMLBodyElement的DOM元素,压栈
- 状态变为in body,同时接收hello world的字符串,接收开始时会创建text节点并插入到body元素的下面,后续接收的字符串或标签等都会依次插入到body元素下面
- 标记器传入body结束标记end tag,进入after body状态
- 标记器传入html技术标记end tag,进入after html状态
- 解析过程结束
- 样式计算
格式化
格式化样式表 =》通过document.styleSheets可以查看格式化后的样式表
标准化
格式化完之后,还需要对其中的一些属性值统一化处理,即标准化
如 em -> px, red -> #ff0000等
计算规则:继承,层叠
格式化和标准化完成之后,接下来就是计算每个节点具体对应的样式了,计算时遵循继承和叠加原则
继承:每个子节点都会默认继承其父节点的样式属性,如果其父节点无样式,则继续往上层查找,最终会继承其浏览器的默认样式
层叠:包含权重,特殊性以及先后顺序使得样式对其节点进行渲染
- 生成布局树
当生成了DOM树和DOM样式后,接下来就需要确定DOM树中每个元素在浏览器上的位置,最终生成布局树
布局树只会考虑显示元素,即display为none时,该元素并不会存在于布局树当中
页面渲染部分
- 建立图层树(Layer Tree)
在页面解析部分,我们生成了DOM树以及DOM样式,并且通过浏览器的布局系统计算出了DOM树中每一个元素对应的坐标位置,从而生成了布局树,看起来应该一切元素已经齐全,是时候要开始进行页面的绘制了,但是对于css以及元素节点的样式而言,远远不止一些简单的布局或者是定位,比如各个元素的层叠,2D,3D的转换以及一些因为结构的复杂会导致各种各样的以外情况,这些仅仅是考布局树是很难准确地渲染出各个元素在浏览器上的位置的,所以在正式绘制之前,我们还需要建立一个可以更加准确地表示各个元素以及其对应的样式的图层树,用来对特定节点进行分层。
一般情况下,子节点的图层默认属于父节点的图层,但是存在一些特殊的情况会使得一个节点提升成为单独的合成层,从而脱离父节点的图层,主要分为以下两种:
显式合成
拥有层叠上下文的节点
// 这里拓展一下z-index,层叠上下文,层叠等级和层叠顺序相关的知识 z-index 用来定义一个元素在z轴上的堆叠顺序 只有当元素定义了position不为static值时,z-index才会生效 判断元素在z轴上的堆叠顺序不仅仅只根据z-index来判断,是结合层叠等级,层叠顺序等的综合排序 在z轴上的层叠等级越高,意味着离屏幕越近 层叠上下文 简单来说,层叠上下文是用来描述元素在z轴上的层叠关系的,层叠上下文元素相比于普通元素而言,其层叠等级更高,离屏幕更近 <html>元素本身具有层叠上下文,称为跟层叠上下文 当一个元素position不为static且设定了z-index时,成为层叠上下文元素 CSS3中的新属性如transform,opacity等也会使得元素成为层叠上下文元素 注:只有当一个元素只有position或z-index时,是不构成 层叠等级 即定义元素在z轴上的层叠顺序优先级 且层叠等级适用于同一个层叠上下文中的元素在z轴上的先后顺序,不同层叠上下文中的元素之间不存在层叠等级的比较 对于其他普通元素而言,层叠等级表示其在z轴的先后顺序 层叠顺序 即用来确定元素的层叠等级 在基本了解了上述概念之后,我们还需要进行结合来实际判断当元素发生层叠时的上下顺序,简单来讲,我们需要遵循如下原则: 1. 首先判断两个元素是否处于同一个层叠上下文中,如果是,则层叠等级高的在上,低的在下 2. 如果两个元素不在同一个层叠上下文中,那么首先判断两个层叠上下文的等级,高的在上,低的在下 3. 如果两个元素所处层叠上下文相同或两个层叠上下文等级相同,且元素本身的层叠等级也相同,则DOM结构中后面的元素等级高,在上
需要剪裁的地方:即如果存在当一个div中的元素内容超出原本设定的大小时,超出的部分就需要被剪裁。如果出现了滚动条,则滚动条会被提升为一个单独的图层
隐式合成
即当层叠等级较低的元素被提升为一个单独的图层之后,等级高于该元素的所有元素都会被提升成独立的图层元素
- 生成绘制列表
当图层树建立完毕,浏览器会将图层一层层进行拆分解析,形成一个个绘制指令,最终会根据这些绘制指令来按照顺序绘制最终的页面
注:在chrome中可以通过more tools里的Layers来查看整个页面被拆分后的具体指令内容
- 生成图块和位图
在实际的绘制过程中,是有一个专门的现成来负责的,称之为
合成线程
。合成线程将图层分块之后,由专门的
栅格化线程池
来将一个个图块转化成位图数据,最终发送给合成线程
用于绘制页面内容注:对于chrome浏览器而言,其底层对于首屏加速的一个优化手段是先用一个低分辨率的图片进行图块合成以及绘制,等待高分辨率的图片加载完成之后,再进行替换,以此来达到更快加载首屏页面的效果
- 显示器显示内容
栅格化操作完成之后,合成线程会生成一个绘制命令,并且发送给浏览器,浏览器接收之后,就会根据命令进行页面的绘制,绘制完成即生成页面之后,将这部分内容发送给显卡,最终显示器显示当前绘制的页面
至此,从输入URL地址到最终页面的显示的完整流程就结束了
补充一下关于重绘和回流相关的知识
首先我们梳理一下整个渲染的基本流程
构建DOM树
=》计算样式
=》生成布局树
=》建立图层树
=》生成绘制列表
(渲染进程当中的主线程中进行)
分图块
=》生成位图
=》发送给浏览器进程
=》发送给显卡缓存
=》显示器显示
(渲染进程当中的合成线程中进行)回流
即当我们修改了DOM结构中元素的几何尺寸等内容后,会引起渲染进程的回流过程
可以引起回流的方式如下:
- 修改元素的width,height,border等内容使得元素的尺寸等发生变化
- 增删或者移动DOM节点
- 读写scroll属性,offset属性以及client属性
- 调用window.getComputedStyle方法
当触发了回流之后,渲染进程需要从构建DOM树开始重新执行后续的操作,也就是相当于重新执行了一遍渲染进程当中的主线程内容,同时处于合成线程中的内容也会重新执行,所以当一个页面中元素结构非常复杂的时候,触发回流很可能会导致页面出现短暂的空白现象从而降低用户的浏览体验
重绘
即当我们对于DOM的修改导致了样式的变化,会引起渲染进程的重绘过程
重绘相比于回流而言,不需要从DOM树部分重新执行,而是仅仅重新计算样式,以及重新生成绘制列表即可,跳过了生成布局树和建立图层树的步骤,但是合成线程中的内容也是需要重新执行的
通过对比我们可以看到,重绘的代价要远远小于回流,所以对于页面加载的优化方面,减少发生回流也是需要考虑的重点,例如可以通过尽可能的使用class类来替代使用style,这样可以避免回流而仅仅引起重绘,减少页面加载的时间
合成
除了重绘和回流,还存在一种直接合成的情况,即跳过了渲染进程中主线程的操作,只需要重新执行合成线程中的流程即可,这种方式也称之为GPU加速
实现合成的方法:CSS3的transform,opacity,filter等属性