输入URL后发生了什么(万字书写ing...)

528 阅读11分钟

本文根据极客时间李冰老师的<浏览器工作原理与实践>,参照MDN,网上各种资料综合而成。尚未整理完毕。

这个过程主要分为两个方面,一个是导航流程,也就是输入url到浏览器获取到资源的部分,一个是渲染流程,也就是浏览器把获取到的资源渲染到页面的部分。

导航流程

首先是导航流程,当用户在地址栏中输入一个查询关键字时,地址栏会判断输入的关键字是搜索内容,还是请求的 URL。如果是搜索内容,地址栏使用浏览器默认搜索引擎合成带搜索关键字的url,如果判断输入内容符合 URL 规则,就会根据规则添加协议合成完整的url,比如输入baidu.com,浏览器会加上https://www。当用户按下回车后,标签页图标进入加载状态,但是页面内容并未替换,需要等待提交文档阶段才会替换。

之后浏览器进程通过IPC进程间通信把url请求发送到网络进程,网络进程接收后,会先在浏览器缓存中查询是否有要请求的文件。如果资源已经缓存,会检测是否是强缓存,主要通过expires和cache-control控制。

  • HTTP1.0提供Expires,值为一个绝对时间表示缓存新鲜日期比如1M
  • HTTP1.1增加了Cache-Control: max-age=,值为以秒为单位的最大新鲜时间

如果命中强缓存就从浏览器读取, 否则命中协商缓存或者没有缓存时,进入网络请求流程。

因为如果想要把数据发送到对方电脑,我们需要知道对方的IP地址和端口号,才能使用TCP协议建立连接。而IP地址是通过DNS解析域名得出。所以浏览器会请求DNS返回域名的对应IP

  1. 浏览器缓存
  2. 本机缓存
  3. hosts文件
  4. 路由器缓存
  5. ISP DNS缓存
  6. DNS递归查询(可能存在负载均衡导致每次IP不一样)本地DNS服务器,
  7. 本地迭代查询
  8. 根DNS服务器(13台)
  9. 顶级域名服务器(.com)
  10. 权威域名服务器(qq.com)

拿到 IP 之后,接下来就需要获取端口号了。通常情况下,如果 URL 没有特别指明端口号,那么 HTTP 协议默认是 80 端口。

当TCP/IP需要的IP和端口获取完毕后浏览器会组装成一个get请求报文。

由于Chrome机制,同一域名最多6个tcp连接,如果当前请求数量小于6,会进入tcp连接,否则需要等待tcp队列。排队等待结束后,建立tcp连接。

  • 客户端发送SYN=1(请求同步),seq=x(客户端序列号)到服务器
  • 服务器返回SYN=1(同意建立连接),seq=y(服务端序列号),ACK=1(你的请求已确认),ack=x+1(期望序列号)
  • 客户端发送ACK=1(你的请求我已经接收),seq=x+1(客户端序列号),ack=y+1(我已经接收,期望你发送y+1

之所以要经历上述三个阶段,是为了浏览器和服务器双方都要知道自己是否能够正常的收发

ACK 0 确认序号无效 1  确认序号成立(请求建立连接)
SYN 1 new connection 建立连接
FIN 1 close 关闭连接
RST 1 distory connection 当前连接异常
PSH 1 数据无需缓存直接给进程
URG 0 紧急指针无效 1 禁忌之中有效,这个包不需要排队,直接进入缓存

连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。如果是post命令还会构建请求体

GET /index.html HTTP1.1

如果头信息中有

Connection:Keep-Alive 

则 TCP 连接在发送后将仍然保持打开状态,这样浏览器就能继续通过这个连接发送请求。

之后断开连接

  1. 主动方发送Fin=1, Ack=Z, Seq= X报文
  2. 被动方发送ACK=X+1, Seq=Z报文
  3. 被动方发送Fin=1, ACK=X, Seq=Y报文
  4. 主动方发送ACK=Y, Seq=X报文

服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应的内容了。

网络进程如果发现响应行的状态码是301或者302(301会把重定向的地址缓存,也就是永久重定向),说明网址变更,需要重定向到其他url,根据响应头的loaction字段信息重定向。如果是200或者304(etag没变说明文件哈希值没变,或者Last-Modified最后修改时间没变),表示浏览器可以继续处理这个请求。之后通过content-type字段告知浏览器如何处理这个请求。详细内容可以在w3c搜mime,那里一大堆。如果 Content-Type 字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是HTML,那么浏览器则会继续进行导航流程。由于 Chrome 的页面渲染是运行在渲染进程中的,所以接下来就需要准备渲染进程了。

打开一个新页面采用的渲染进程策略就是:

  • 通常情况下,打开新的页面都会使用单独的渲染进程;
  • 如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点的话,那么 B 页面复用 A 页面的渲染进程;如果是其他情况,浏览器进程则会为 B 创建一个新的渲染进程。

渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档(“文档”是指 URL 请求的响应体数据)阶段。

  • “提交文档”的消息是由浏览器进程发出的,渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”。
  • 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。
  • 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。

一旦文档被提交,渲染进程便开始页面解析和子资源加载,页面生成完成,渲染进程会发送一个消息给浏览器进程,浏览器接收到消息后,会停止标签图标上的加载动画。之后进入渲染阶段。

按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。

构建DOM树dom

浏览器无法直接使用HTML,需要把HTML转化为浏览器可以理解的DOM树结构。

构建 DOM 树的输入内容是一个非常简单的 HTML 文件,然后经由 HTML 解析器解析,最终输出树状结构的 DOM,和 HTML 不同的是,DOM 是保存在内存中树状结构,可以通过 JavaScript 来查询或修改其内容。

DOM树构建完毕后就进入节点样式计算

样式计算style

样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成。

首先是把 CSS 转换为浏览器能够理解的stylesheets,这个过程先把字节流转换成Tokenizing标记,根据标记创建节点,最后根据节点组成stylesheets,之后转换样式表中的属性值,使其标准化,因为CSS 文本中有很多属性值,如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,比如2em 被解析成了 32px,red 被解析成了 rgb(255,0,0),bold 被解析成了 700等等。最后计算出 DOM 树中每个节点的具体样式,比如继承规则,font-size的继承,color的继承,还有层叠优先级规则。这个阶段最终输出的内容是每个 DOM 节点的样式。

布局layout

计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。

首先是创建布局树,遍历 DOM 树中的所有可见节点,并把这些节点加到布局中;而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树。之后进行布局计算,布局的计算过程非常复杂,我们这里先跳过不讲,等到后面章节中我再做详细的介绍。计算布局树节点的坐标位置。在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方,因为在布局阶段并没有清晰地将输入内容和输出内容区分开来。针对这个问题,Chrome 团队正在重构布局代码,下一代布局系统叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。

分层layer

元素有了层叠上下文的属性或者需要被剪裁(文字溢出、文字内容有滚动条),满足这任意一点,就会被提升成为单独一层。

如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果**,**渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。

图层绘制(paint)

渲染引擎把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。所以在图层绘制阶段,输出的内容就是这些待绘制列表。

分块

当图层的绘制列表准备好之后,主线程会把该绘制列表**提交(commit)**给合成线程,绘制列表生成后,合成线程会把图层划分成一个个的图块。

栅格化(raster)操作

出于性能考虑,绘制出所有图层内容的话,会产生太大的开销,而且用户也不一定完全浏览完一个网页,所以合成线程会优先选择viewport视口附近的图块,把这些图块生成位图。栅格化就是把图块生成位图的过程。图块则是栅格化进行的最小单位,通常栅格化过程是用gpu加速生成的。

合成显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,并把这个命令提交给浏览器进程,浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

而我们平时说的重排,需要重新触发布局layout之后的一系列流水线,重绘则是跳过了布局和分层,从绘制开始触发后面的一系列流水线。而使用css3的一些属性,则是会直接跳过布局、分层、绘制的阶段,直接进入合成线程执行后面的一系列流水线。