我们需要在地址栏中输入一个查询关键字时,地址栏会先判断输入的关键字是搜索内容,还是请求的 URL。
如果是搜索内容字,地址栏会使用浏览器默认的搜索引擎,来合成新的搜索关键字的 URL。
如果是符合 URL 规则,那么浏览器会根据规则,加上协议,合并成为完整的 URL。
csdn.net 合并为 https://www.csdn.net
并且在发起真正的 csdn.net 请求之前,浏览器会给当前(即将跳转的页面)一次执行 beforeunload 事件的机会,允许当前页面执行一些清理性的操作,有的也会进行一次询问用户是否要跳转页面的弹框,可以用来取消导航。
接下来浏览器需要提交文档,在这个阶段浏览器是处于一个即将跳转页面的页面刷新。
提交文档阶段
此时此刻,你在浏览器地址栏输入了百度的网址:
https://www.baidu.com/
网络请求
1. 浏览器会构件请求行:
// 请求方法是 GET , 请求路径为根路径下的index.html,HTTP协议版本为 1.1
GET /index.html HTTP/1.1
浏览器进程会通过进程通信 IPC ,把 URL 请求发送至网络进程,网络进程在收到请求后,会发起真正的 URL 请求。
2. 查找强缓存
网络进程在发起请求资源前,先检查强缓存,如果命中直接使用,否则就进入下一步。
3. DNS 解析
由于我们输入的是域名,而数据包是通过 *IP 地址* 传给对方的。因此我们需要得到域名对应的 IP 地址 。这个过程需要依赖一个服务系统,这个系统将域名和 IP 一一映射,我们将这个系统叫做 DNS (域名系统)。
浏览器提供了 **DNS数据缓存功能**。如果一个域名已经解析过了,那么会把解析的结果缓存起来,瑕疵处理直接走缓存,不需要经过 <u>DNS解析</u>。
4. 建立 TCP 连接。
Chrome 在同一个域名下要求同时最多只能有 6 个 TCP 连接,超过 6 个的话剩下的请求就得等待。
假设现在是小于 6 个连接的,我们进入 TCP 连接的建立阶段。
`TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。`
TCP 通过三次握手,数据包校验、四次挥手 来保证数据传输的可靠性。
5. 发送 HTTP 请求。
现在的话,以及建立好了 TCP 连接,浏览器可以和服务器进行通信,发送 HTTP 请求。浏览器会向服务器发送请求行。
// 请求方法是 GET , 请求路径为根路径下的index.html,HTTP协议版本为 1.1
GET /index.html HTTP/1.1
同时加上请求头,比如我们常见的就是关于缓存的请求头数据 Cache-Control、If-Modified-Since 都可以被放入请求头作为缓存标识。还有一些其他的属性。
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: keep-alive
Cookie: /* 省略cookie信息 */
Host: www.baidu.com
Pragma: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1
最后是请求体,请求体只有在POST方法下存在,常见的场景是表单提交。
6. HTTP 请求加上 TCP 头部,包括原端口号、目的程序端口号和用与校验完整性的序号,向下传输。
7. 底层通过物理网络传输给目标服务器主机。
8. 目标服务器主机网络层接收到数据包,解析出IP头部,识别出数据部分,将解开的数据包向上传输到传输层.
9. 目的服务器主机传输层获取到数据包,解析出TCP头部,识别端口,将解开的数据包向上传输到应用层。
网络响应
HTTP 请求到达服务器,服务器进行对应的处理。最后要把数据传给浏览器,也就是返回网络响应。
跟请求部分类似,网络响应具有三个部分:响应行、响应头和响应体。
响应行类似下面这样:
HTTP协议版本、状态码和状态描述
HTTP/1.1 200 OK
响应头包含了服务器及其返回数据的一些信息, 服务器生成数据的时间、返回的数据类型以及对即将写入的Cookie信息。
Cache-Control: no-cache
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html;charset=utf-8
Date: Wed, 04 Dec 2019 12:29:13 GMT
Server: apache
Set-Cookie: rsv_i=f9a0SIItKqzv7kqgAAgphbGyRts3RwTg%2FLyU3Y5Eh5LwyfOOrAsvdezbay0QqkDqFZ0DfQXby4wXKT8Au8O7ZT9UuMsBq2k; path=/; domain=.baidu.com
1. 如果需要重定向,HTTP 直接返回 HTTP 响应数据的状态 301 或者 302 同时在请求头的 Location 字段中附上重定向地址,浏览器会根据 `状态码` 和 `Location` 进行重定向操作,然后按照重定向地址按照上面步骤重新发起请求。
如果不是重定向,首先服务器会根据请求头中的` If-None-Match` 的值来判断请求的资源是否被更新,如果没有更新,就返回 `304` 状态码,相当于告诉浏览器之前的缓存还可以使用,就不返回新数据了;否则,返回新数据,`200` 的状态码,并且如果想要浏览器缓存数据的话,就在相应头中加入字段:`Cache-Control:Max-age=2000`。
2. 响应数据又顺着应用层——传输层——网络层——网络层——传输层——应用层的顺序返回到网络进程。
响应完成之后怎么办?TCP 连接就断开了吗?
1. 这时候要判断 `Connection` 字段, 如果请求头或响应头中包含 `Connection: Keep-Alive` ,表示建立了持久连接,这样TCP连接会一直保持,之后请求统一站点的资源会复用这个连接。
否则断开TCP连接, 请求-响应流程结束。
2. 网络进程将获取到的数据包进行解析,根据响应头中的 `Content-type` 来判断响应数据的类型,如果是字节流类型,就将该请求交给下载管理器,该导航流程结束,不再进行;如果是 `text/html` 类型,就通知浏览器进程获取到文档准备渲染进程。
3. 默认情况下,Chrome 会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。但是,也有一些例外,`在同一站点下`,浏览器会让多个页面直接运行在同一个渲染进程中。
4. 渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了 `提交文档阶段` 。
- 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“ `提交文档` ”的消息;
- 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“ `管道` ”。
- 等文档数据传输完成之后,渲染进程会返回“ `确认提交` ”的消息给浏览器进程;
- 浏览器进程在收到“确认提交”的消息后,会 `更新浏览器界面` 状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。
完成了网络请求和响应,如果响应头中 Content-Type 的值是 text/html ,接下来就是浏览器的 `解析` 和 `渲染` 工作量。
渲染阶段:
目的:左边输入的是 HTML、CSS、JavaScript 数据,这些数据经过中间渲染模块的处理,最终输出为屏幕上的像素。
按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。
1. 构建 DOM 树
浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。
DOM 树的构建过程:
构建 DOM 树的输入内容是一个非常简单的 HTML 文件,然后经由 HTML 解析器解析,最终输出树状结构的 DOM。DOM 和 HTML 内容几乎是一样的,但是和 HTML 不同的是,DOM 是保存在内存中树状结构,可以通过 JavaScript 来查询或修改其内容。
2. 样式计算(Recalculate Style)
1. 把 CSS 转换为浏览器能够理解的结构
当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。
并且该结构同时具备了查询和修改功能,这会为后面的样式操作提供基础。
2. 转换样式表中的属性值,使其标准化
3. 计算出 DOM 树中每个节点的具体样式
首先是 CSS 继承。CSS 继承就是每个 DOM 节点都包含有父节点的样式。
样式计算过程中的第二个规则是样式层叠。层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在 CSS 处于核心地位,CSS 的全称“层叠样式表”正是强调了这一点 。
这个阶段最终输出的内容是每个 DOM 节点的样式,并被保存在 ComputedStyle 的结构内。
3. 布局计算
1. 创建布局树
你可能注意到了 DOM 树还含有很多不可见的元素,比如 head 标签,还有使用了 display:none 属性的元素。所以在显示之前,我们还要额外地构建一棵只包含可见元素布局树。
2. 布局计算
现在我们有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了。会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容。
4. 分层
因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。
浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。
并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
满足下面两点中任意一点的元素就可以被提升为单独的一个图层:
第一点,拥有层叠上下文属性的元素会被提升为单独的一层。明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性。
第二点,需要剪裁(clip)的地方也会被创建为图层。overflow: auto;
5. 图层绘制
渲染引擎会对图层树中的每个图层进行绘制,那么接下来我们看看渲染引擎是怎么实现图层绘制的?
渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,如下图所示:
6. 栅格化(raster)操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系:
当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,那么接下来合成线程是怎么工作的呢?
合成线程会将图层划分为图块,这些图块的大小通常是 256x256 或者 512x512,如下图所示。然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:
栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。
从图中可以看出,渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。
5. 合成和显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了。