说一说从 URL 输入到页面呈现到底发生了什么?

2,861 阅读13分钟

前言

这是面试过程中一道高频考题。

从面试官的角度思考:

  • 出现频繁,可能是因为面试官通常喜欢问一些考察可深可浅的题目
  • 很多面试管喜欢根据我们应聘者的考题回答中,甚至我们随口说到的知识点,继续追问

基本回答:

  • 浏览器解析 URL 获取协议,主机,端口, path
  • 浏览器获取主机 IP 地址
  • 建立 TCP 连接,然后发送 HTTP 请求
  • 服务器将响应报文通过 TCP 连接发送回浏览器,浏览器接收 HTTP 响应,根据资源类型决定如何处理(假设资源为 HTML 文档)
  • 解析 HTML 文档,构件 DOM 树,下载资源,构造 CSSOM 树,执行 js 脚本,最后展现出来给用户

如果应聘者只回答了上述步骤,很多关键步骤(前端应该了解的知识点)没有提及,很有可能达不到面试官想要的回答效果。

笔者针对一些关键步骤,具体展开说明。让这道题成为我们面试考卷中的加分项

网络请求

构建请求

浏览器会构建请求行:

// 请求方法是 GET,路径为根路径,HTTP 协议版本为 1.1
GET / HTTP/1.1

然后根据 Cache-controlExpires 字段,检查强缓存,如果命中直接使用,否则进入下一步。关于强缓存,如果不清楚可以参考下图:

DNS 解析

由于我们输入的是域名,而数据包是通过 IP 地址传给对方的。因此我们需要得到域名对应的 IP 地址。这个过程需要依赖一个服务系统,这个系统将域名和 IP 一一映射,我们将这个系统就叫做 DNS (域名系统)。

DNS 协议提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务。得到具体 IP 的过程就是 DNS 解析。

DNS 是一个网络服务器,我们的域名解析简单来说就是在 DNS 上记录一条信息记录。

例如 baidu.com 220.114.23.56(服务器外网IP地址)80(服务器端口号)

浏览器通过域名去查询 URL 对应的 IP

  • 浏览器缓存:浏览器会按照一定的频率缓存 DNS 记录
  • 操作系统缓存:如果浏览器缓存中找不到需要的 DNS 记录,那就去操作系统中找
  • 路由缓存:路由器也有 DNS 缓存
  • ISPDNS 服务器:ISP 是互联网服务提供商( Internet Service Provider )的简称,ISP 有专门的 DNS 服务器应对 DNS 查询请求
  • 根服务器:ISPDNS 服务器还找不到的话,它就会向根服务器发出请求,进行递归查询(DNS 服务器先问根域名服务器 .com 域名服务器的 IP 地址,然后再问 .baidu 域名服务器,依次类推)

建立 TCP 连接

TCP 三次握手的过程如下:

  • 客户端发送一个带 SYN=1Seq=X 的数据包到服务器端口(第一次握手,由浏览器发起,告诉服务器我要发送请求了)
  • 服务器发回一个带 SYN=1ACK=X+1Seq=Y 的响应包以示传达确认信息(第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧)
  • 客户端再回传一个带 ACK=Y+1Seq=Z 的数据包,代表“握手结束”(第三次握手,由浏览器发送,告诉服务器,我马上就发了,准备接受吧)

谢希仁著《计算机网络》中讲“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。

发送 HTTP 请求

现在 TCP 连接建立完毕,浏览器可以和服务器开始通信,即开始发送 HTTP 请求。浏览器发 HTTP 请求要携带三样东西:请求行请求头请求体

1.请求行包含请求方法、URL、协议版本

  • 请求方法包含 8 种:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE
  • URL 即请求地址,由 <协议>://<主机>:<端口>/<路径>?<参数> 组成
  • 协议版本即 HTTP 版本号
POST /user.html HTTP/1.1

2.请求头包含请求的附加信息,由关键字/值对组成,如下

// 服务器可以接受的文件格式
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng;q=0.8,application/signed-exchange;v=b3
// 指定浏览器可以支持的 Web 服务器返回的内容压缩编码类型
Accept-Encoding: gzip, deflate, br
// 浏览器支持的语言
Accept-Language: zh-CN,zh;q=0.9
// 缓存机制
Cache-Control: no-cache
// 是否需要持久连接
Connection: keep-alive
// 发送该请求域名下所有 Cookie 值到服务器
Cookie: /* 省略cookie信息 */
// 指定请求的服务器的域名和端口号
Host: www.baidu.com
Pragma: no-cache
Upgrade-Insecure-Requests: 1
// 用户代理 UA,包含发出请求的用户信息
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

3.请求体,可以承载多个请求参数的数据,包含回车符、换行符和请求数据,一般在 POST 方法下存在。

网络响应

跟请求部分类似,网络响应具有三个部分:响应行响应头响应体

1.响应行包含:协议版本,状态码,状态码描述

HTTP/1.1 200 OK

状态码规则如下:

  • 1xx:指示信息--表示请求已接收,继续处理
  • 2xx:成功--表示请求已被成功接收、理解、接受
  • 3xx:重定向--要完成请求必须进行更进一步的操作
  • 4xx:客户端错误--请求有语法错误或请求无法实现
  • 5xx:服务器端错误--服务器未能实现合法的请求

2.响应头部包含响应报文的附加信息,由 名/值 对组成,如下:

// 缓存机制
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
// Web 服务器软件名称
Server: apache
// 由服务器端向客户端发送 cookie
Set-Cookie: rsv_i=f9a0SIItKqzv7kqgAAgphbGyRts3RwTg%2FLyU3Y5Eh5LwyfOOrAsvdezbay0QqkDqFZ0DfQXby4wXKT8Au8O7ZT9UuMsBq2k; path=/; domain=.baidu.com

这里注意下 Set-Cookie 中关于网络安全方面的两个值:HttpOnlySameSite

设置了 HttpOnly 属性的 cookie 不能使用 JavaScript 经由 Document.cookie 属性、XMLHttpRequest 和 Request APIs 进行访问,以防范跨站脚本攻击(XSS)。

SameSite=Lax 允许服务器设定一则 cookie 不随着跨域请求一起发送,这样可以在一定程度上防范跨站请求伪造攻击(CSRF)。

3.响应主体包含回车符、换行符和响应返回数据,并不是所有响应报文都有响应数据

响应完成之后要判断 Connection 字段,如果请求头或响应头中包含 Connection: Keep-Alive ,表示建立了持久连接,这样 TCP 连接会一直保持,之后请求统一站点的资源会复用这个连接。 否则断开 TCP 连接, 请求-响应流程结束。

总结浏览器端的网络请求过程:

浏览器解析渲染页面

浏览器解析渲染页面分为以下五个步骤:

  • 根据 HTML 解析出 DOM
  • 根据 CSS 解析生成 CSS 规则树
  • 结合 DOM 树和 CSS 规则树,生成渲染树
  • 根据渲染树计算每一个节点的信息
  • 根据计算好的信息绘制页面

回流时,以上流程会重新走一遍。重绘时,会重新计算样式,跳过中间步骤直接生成绘制列表。可见,重绘不一定导致回流,但回流一定发生了重绘。

构建 DOM 树

  • HTML 语法定义

    HTML 的词汇与句法定义在 w3c 组织创建的规范中。当前版本是 HTML4HTML5 的工作正在进行中。

  • 不是上下文无关语法

    在对解析器的介绍中看到,语法可以用类似 BNF 的格式规范地定义。不幸的是所有常规解析器的讨论都不适用于 HTML (我提及它们并不是为了娱乐,它们可以用于解析 CSSJavaScript )。HTML 无法用解析器所需的上下文无关的语法来定义。过去 HTML 格式规范由 DTD ( Document Type Definition ) 来定义,但它不是一个上下文无关语法。

    HTMLXML 相当接近。XML 有许多可用的解析器。HTML 还有一个 XML 变种叫 XHTML ,那么它们主要区别在哪里呢?区别在于 HTML 应用更加”宽容”,它容许你漏掉一些开始或结束标签等。它整个是一个“软”句法,不像 XML 那样严格死板。 总的来说这一看似细微的差别造成了两个不同的世界。一方面这使得 HTML 很流行,因为它包容你的错误,使网页作者的生活变得轻松。另一方面,它使编写语法格式变得困难。所以综合来说,HTML 解析并不简单,现成的上下文相关解析器搞不定,XML 解析器也不行。

  • 解析算法

    • 标记化
    • 建树

    对应的两个过程就是分词和语法分析(参考Babel 编译的解析过程)。

    这里举例重点介绍下 HTML5容错机制

    • 使用 </br> 而不是 <br>

      if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
          reportError(MalformedBRError);
          t->beginTag = true;
      }

      全部换为 <br> 的形式。

    • 表格离散

      <table>
          <table>
              <tr><td>inner table</td></tr>
          </table>

          <tr><td>outer table</td></tr>
      </table>

      WebKit 会自动转换为:

      <table>
          <tr><td>outer table</td></tr>
      </table>
      <table>
          <tr><td>inner table</td></tr>
      </table>

    • 表单元素嵌套

      这时候直接忽略里面的 form

样式计算

CSS 样式来源一般为三种:

  • link 标签引用
  • style 标签中样式
  • 元素内嵌 style 属性
格式化样式表

浏览器无法直接识别 CSS 样式文本,这里渲染引擎接收到 CSS 文本之后将其转化为一个结构话的对象,即 styleSheets

可以在浏览器控制台输入 document.styleSheets 来查看这个最终结构(包含上述三种 CSS 来源)。

标准化样式属性

有一些渲染引擎不容易直接理解的 CSS 样式数值,需要在计算样式之前将它们标准化。如:em -> pxred -> #ff0000bold -> 700 等等。

计算每个节点的具体样式

计算具体样式主要遵循两个规则:继承层叠

  • 继承:

    每个子节点都会默认继承父节点的样式属性,如果父节点中没有找到,就采用浏览器默认样式,也叫 UserAgent样式

  • 层叠:

    CSS 的层叠性体现在,最终的样式取决与各个属性共同作用的结果。

计算完样式之后,所有样式值会被挂载到 window.getComputedStyle 中,也就是可以通过 JS 获取计算后的样式。

生成布局树

布局树生成主要分两部:

  • 遍历生成的 DOM 树节点,并把它们添加到布局树中
  • 计算布局树节点的坐标位置

布局树只包含可见元素,对于 head 标签和设置了 display: none 的元素将不会被放入其中。

如果想了解布局的细节,可以读一读人人 FED 团队的文章从Chrome源码看浏览器如何layout布局

构建图层树

这里分两种情况,一种是显式合成,一种是隐式合成

显式合成

一、拥有层叠上下文的节点

层叠上下文也基本上是有一些特定的 CSS 属性创建的,一般有以下情况:

  1. HTML 根元素本身就具有层叠上下文

  2. 普通元素设置 position 不为 static 并且设置了 z-index 属性,会产生层叠上下文

  3. 元素的 opacity 值不是 1

  4. 元素的 transform 值不是 none

  5. 元素的 filter 值不是 none

  6. 元素的 isolation 值是 isolate

  7. will-change 指定的属性值为上面任意一个

二、需要剪裁的地方

比如一个 div,你只给他设置 100 * 100 像素的大小,而你在里面放了非常多的文字,那么超出的文字部分就需要被剪裁。当然如果出现了滚动条,那么滚动条会被单独提升为一个图层。

隐式合成

简单说就是层叠等级低的节点被提升为单独的图层之后,那么所有层叠等级比它高的节点都会成为一个单独的图层。

这个隐式合成其实隐藏着巨大风险,如果在一个大型应用中,当一个 z-index 比较低的元素被提升为单独图层之后,层叠在它上面的元素统统会被提升为单独的图层,可能会增加上千个图层,大大增加内存压力,甚至直接让页面崩溃。这就是层爆炸的原理

当需要 repaint 时,只需要 repaint 本身,而不会影响到其他层。

生成绘制列表

渲染引擎会将图层的绘制拆分成一个个绘制指令,比如先画背景、再描绘边框......然后将这些指令按顺序组合成一个待绘制列表。

大家可以 F12 打开 Chrome 开发者工具,在设置栏展开 more tools ,然后选择 Layers 面板,就能看到绘制列表了。

后面就是渲染进程的主线程把绘制列表提交给合成线程。然后合成线程选择视口附近的图块,把它交给栅格化线程池生成位图。

栅格化操作完成后,合成线程会生成一个绘制指令 DrawQuad,并发送给浏览器进程。浏览器进程中的 viz 组件 接收到命令,把页面内容绘制到内存,也就是生成了页面。

断开连接

当数据传送完毕,需要断开 TCP 连接,此时发起四次挥手。

  • 发起方往被动方发送报文,Fin、Ack、Seq,表示已经没有数据传输了。并进入 FIN_WAIT_1 状态。(请求报文发送完成)
  • 被动方发送报文,Ack、Seq,表示同意关闭请求。此时主机发起方进入 FIN_WAIT_2 状态。(请求报文接受完成)
  • 被动方向发起方发送报文段,Fin、Ack、Seq,请求关闭连接。并进入 LAST_ACK 状态。(响应报文发送完成)
  • 发起方向被动方发送报文段,Ack、Seq。然后进入等待 TIME_WAIT 状态。被动方收到发起方的报文段以后关闭连接。发起方等待一定时间未收到回复,则正常关闭。(响应报文接受完成)

参考文章

感谢

如果本文对你有帮助,就点个赞支持下吧!感谢阅读。