从输入 URL 到页面展示,中间发生了什么?

·  阅读 400

这是一道经典的面试题,能比较全面地考察应聘者知识的掌握程度,其中涉及到了网络、操作系统、Web 等一系列的知识。最近在学习浏览器工作原理时学到这个,在这里做个总结。

一、用户输入

当用户在地址栏中输入一个查询关键字时,以 Chrome 浏览器为例地址栏会判断输入的关键字是搜索内容,还是请求的 URL。

  • 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的 URL。
  • 如果判断输入内容符合 URL 规则,比如输入的是 juejin.cn 那么地址栏会根据规则,把这段内容加上协议,合成为完整的 URL,如:https://juejin.cn

当用户输入关键字并键入回车之后,浏览器便进入下图的状态: 从图中可以看出,当浏览器刚开始加载一个地址之后,标签页上的图标便进入了加载状态。但此时图中页面显示的依然是之前打开的页面内容,并没立即替换为掘金的页面。因为需要等待提交文档阶段,页面内容才会被替换。

二、URL 请求过程

接下来,便进入了页面资源请求过程。这时,浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程,网络进程接收到 URL 请求后,会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。

这请求前的第一步是要进行 DNS 解析(览器提供了DNS 数据缓存服务,如果某个域名已经解析过了,那么浏览器会缓存解析的结果,以供下次查询时直接使用,这样会减少一次网络请求),以获取请求域名的服务器 IP 地址。如果请求协议是 HTTPS,那么还需要建立 TLS 连接。

接下来就是利用 IP 地址和服务器建立 TCP 连接。也就是三次握手,具体步骤为下:

  1. 客户端(通过执行connect函数)向服务器端发送一个SYN包,请求一个主动打开。该包携带客户端为这个连接请求而设定的随机数A作为消息序列号。
  2. 服务器端收到一个合法的SYN包后,把该包放入SYN队列中;回送一个SYN/ACK。ACK的确认码应为A+1,SYN/ACK包本身携带一个随机产生的序号B。
  3. 客户端收到SYN/ACK包后,发送一个ACK包,该包的序号被设定为A+1,而ACK的确认码则为B+1。然后客户端的connect函数成功返回。当服务器端收到这个ACK包的时候,把请求帧从SYN队列中移出,放至ACCEPT队列中;这时accept函数如果处于阻塞状态,可以被唤醒,从ACCEPT队列中取出ACK包,重新创建一个新的用于双向通信的sockfd,并返回。

连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。

1、重定向

在接收到服务器返回的响应头后,网络进程开始解析响应头,如果发现返回的状态码是 301 或者 302,那么说明服务器需要浏览器重定向到其他 URL。这时网络进程会从响应头的 Location 字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,一切又重头开始了。
比如,我们在终端里输入以下命令:

curl -I http://juejin.cn
复制代码

curl -I + URL的命令是接收服务器返回的响应头的信息。执行命令后,我们看到服务器返回的响应头信息如下: 从图中可以看出,掘金服务器会通过重定向的方式把所有 HTTP 请求转换为 HTTPS 请求。也就是说你使用 HTTP 向掘金服务器请求时,服务器会返回一个包含有 301 或者 302 状态码响应头,并把响应头的 Location 字段中填上 HTTPS 的地址,这就是告诉了浏览器要重新导航到新的地址上。

下面我们再使用 HTTPS 协议对掘金发起请求,看看服务器的响应头信息是什么样子的。

curl -I https://juejin.cn
复制代码

我们看到服务器返回如下信息: 在导航过程中,如果服务器响应行的状态码包含了 301、302 一类的跳转信息,浏览器会跳转到新的地址继续导航;如果响应行是 200,那么表示浏览器可以继续处理该请求。

2、响应数据类型处理

在处理完跳转信息之后,然后浏览器根据Content-Type属性区分是一个下载类型,还是正常的 HTML 页面Content-Type 是 HTTP 头中一个非常重要的字段, 它告诉浏览器服务器返回的响应体数据是什么类型,然后浏览器会根据 Content-Type 的值来决定如何显示响应体的内容。

这里我们还是以掘金为例,看看掘金官网返回的 Content-Type 值是什么。在终端输入以下命令:

curl -I https://juejin.cn
复制代码

返回信息如下图: 从图中可以看到,响应头中的 Content-type 字段的值是 text/html,这就是告诉浏览器,服务器返回的数据是HTML 格式。

接下来我们再来利用 curl 来请求安装包的地址,如下所示:

curl -I https://pm.myapp.com/invc/xfspeed/qqpcmgr/download/Weixin_Download1100119872.exe
复制代码

从返回的响应头信息来看,其 Content-Type 的值是 application/octet-stream,显示数据是字节流类型的,通常情况下,浏览器会按照下载类型来处理该请求。

如果服务器配置 Content-Type 不正确,比如将 text/html 类型配置成 application/octet-stream 类型,那么浏览器可能会曲解文件内容,比如会将一个本来是用来展示的页面,变成了一个下载文件。

所以,不同 Content-Type 的后续处理流程也截然不同。如果 Content-Type 字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的流程就此结束。但如果是HTML,那么浏览器则会继续进行。由于 Chrome 的页面渲染是运行在渲染进程中的,所以接下来就需要准备渲染进程了。

3、准备渲染进程

默认情况下,Chrome 会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。但是,也有一些例外,在某些情况下,浏览器会让多个页面直接运行在同一个渲染进程中。

Chrome 的默认策略是,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫 process-per-site-instance

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

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

渲染进程准备好之后,下一步就进入了提交文档阶段。

4、提交文档

这里的“文档” 是指 URL 请求的响应体数据

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

更新内容如下:

这也就解释了为什么在浏览器的地址栏里面输入了一个地址后,之前的页面没有立马消失,而是要加载一会儿才会更新页面。

到这里,一个完整的请求流程就“走”完了,这之后就要进入渲染阶段了。

三、渲染阶段

1、构建 DOM 树

因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。 树这种结构非常像我们现实生活中的“树”,其中每个点我们称为节点,相连的节点称为父子节点。

为了更加直观地理解 DOM 树,你可以打开 Chrome 的“开发者工具”,选择“Console”标签来打开控制台,然后在控制台里面输入document后回车,这样你就能看到一个完整的 DOM 树结构,如下图所示: 图中的 document 就是 DOM 结构,你可以看到,DOM 和 HTML 内容几乎是一样的,但是和 HTML 不同的是,DOM 是保存在内存中树状结构,可以通过 JavaScript 来查询或修改其内容。

现在我们已经生成 DOM 树了,但是 DOM 节点的样式我们依然不知道,要让 DOM 节点拥有正确的样式,这就需要样式计算了。

2、样式计算(Recalculate Style)

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

(1)把 CSS 转换为浏览器能够理解的结构

CSS 样式来源主要有三种:

  • 通过 link 引用的外部 CSS 文件
  • <style>标记内的 CSS
  • 元素的 style 属性内嵌的 CSS

和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。

我们可以在 Chrome 控制台中查看其结构,只需要在控制台中输入 document.styleSheets,然后就看到如下图所示的结构:

从图中可以看出,这个样式表包含了很多种样式,已经把那三种来源的样式都包含进去了。渲染引擎会把获取到的 CSS 文本全部转换为 styleSheets 结构中的数据,并且该结构同时具备了查询和修改功能,这会为后面的样式操作提供基础。

(2)转换样式表中的属性值,使其标准化

现在我们已经把现有的 CSS 文本转化为浏览器可以理解的结构了,那么接下来就要对其进行属性值的标准化操作。

要理解什么是属性值标准化,你可以看下面这样一段 CSS 文本:

body { font-size: 2em }
p {color:blue;}
span  {display: none}
div {font-weight: bold}
div  p {color:green;}
div {color:red; }
复制代码

可以看到上面的 CSS 文本中有很多属性值,如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。 那标准化后的属性值是什么样子的?

body { font-size: 32px }
p {color: rgb(0, 0, 255);}
span  {display: none}
div {font-weight: 700}
div  p {color: rgb(0, 128, 0);}
div {color: rgb(255, 0, 0); }
复制代码

可以看到,2em 被解析成了 32px,red 被解析成了 rgb(255,0,0),bold 被解析成了 700……

(3)计算出 DOM 树中每个节点的具体样式

如果我们想了解每个 DOM 元素最终的计算样式,可以打开 Chrome 的“开发者工具”,选择第一个“element”标签,然后再选择“Computed”子标签,如下图所示: 上图红色方框中显示了 html.body.div.p 标签的 ComputedStyle 的值。你想要查看哪个元素,点击左边对应的标签就可以了。

3、布局阶段

现在,我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。

Chrome 在布局阶段需要完成两个任务:创建布局树和布局计算。

(1)创建布局树

你可能注意到了 DOM 树还含有很多不可见的元素,比如 head 标签,还有使用了 display:none 属性的元素。所以在显示之前,我们还要额外地构建一棵只包含可见元素布局树。

为了构建布局树,浏览器大体上完成了下面这些工作:

  • 遍历 DOM 树中的所有可见节点,并把这些节点加到布局中;
  • 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树。
(2)布局计算

现在我们有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了。布局的计算过程非常复杂,我还没有学会。

4、分层

因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。如果你熟悉 PS,相信你会很容易理解图层的概念,正是这些图层叠加在一起构成了最终的页面图像。

要想直观地理解什么是图层,你可以打开 Chrome 的“开发者工具”,选择“Layers”标签,就可以可视化页面的分层情况,如下图所示: 从上图可以看出,渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面。

通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。 但不管怎样,最终每一个节点都会直接或者间接地从属于一个层。

那么需要满足什么条件,渲染引擎才会为特定的节点创建新的层呢?通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层。

  1. 拥有层叠上下文属性的元素会被提升为单独的一层。
  2. 需要剪裁(clip)的地方也会被创建为图层。

5、图层绘制

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,那么接下来我们看看渲染引擎是怎么实现图层绘制的?

试想一下,如果给你一张纸,让你先把纸的背景涂成蓝色,然后在中间位置画一个红色的圆,最后再在圆上画个绿色三角形。你会怎么操作呢?

通常,你会把你的绘制操作分解为三步:

  1. 绘制蓝色背景;
  2. 在中间绘制一个红色的圆;
  3. 再在圆上绘制绿色三角形。

渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。

绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。

你也可以打开“开发者工具”的“Layers”标签,选择“document”层,来实际体验下绘制列表,如下图所示: 在该图中,区域 1 就是 document 的绘制列表,拖动区域 2 中的绿色标注的进度条可以重现列表的绘制过程。

6、栅格化(raster)操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,那么接下来合成线程是怎么工作的呢?

那我们得先来看看什么是视口?

通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。

在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。

基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512,然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。

通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。

7、合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。

浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

总结

  1. 用户输入url并回车
  2. 浏览器进程检查url,组装协议,构成完整的url
  3. 浏览器进程通过进程间通信(IPC)把url请求发送给网络进程
  4. 网络进程接收到url请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程,请求结束
  5. 如果没有,网络进程向web服务器发起http请求(网络请求):
    1. 进行DNS解析,获取服务器ip地址,端口号
    2. 利用ip地址和服务器建立tcp连接(三次握手)
    3. 构建请求信息并发送
    4. 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容
  6. 网络进程解析响应流程:
    1. 检查状态码,如果是301/302,则需要重定向,从Location自动中读取地址,重新进行第4步,如果是200,则继续处理请求
    2. 检查响应类型Content-Type,如果是字节流类型,则将该请求提交给下载管理器,请求结束,不再进行后续的渲染,如果是html则通知浏览器进程准备渲染进程准备进行渲染
  7. 准备渲染进程
    1. 浏览器进程检查当前url是否和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程
  8. 传输数据、更新状态
    1. 渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息和网络进程建立传输数据的“管道”
    2. 渲染进程接收完数据后,向浏览器发送“确认提交”
    3. 浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏url、前进后退的历史状态、更新web页面
  9. 浏览器开始渲染页面
    1. 将 HTML 内容转换为能够读懂的 DOM 树结构
    2. 将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式
    3. 创建布局树,并计算元素的布局信息
    4. 对布局树进行分层,并生成分层树
    5. 为每个图层生成绘制列表,并将其提交到合成线程
    6. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图
    7. 合成线程发送绘制图块命令DrawQuad给浏览器进程
    8. 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上
分类:
前端
标签: