浏览器是我们工作过程中的重要宿主,了解浏览器的工作原理会有助于我们更快的解决开发过程中浏览器的问题以及对页面在浏览器中的优化起到帮助作用。实际上,对浏览器的实现者来说,他们做的事情,就是把一个URL变成一个屏幕上显示的网页。整个过程如下:
- 浏览器首先使用HTTP协议或者HTTPS协议,向服务端请求页面;
- 把请求回来的HTML代码经过解析,构建成DOM树;
- 计算DOM树上的CSS属性;
- 最后根据CSS属性对元素逐个进行渲染,得到内存中的位图;
- 一个可选的步骤是对位图进行合成,这会极大地增加后续绘制的速度;
- 合成之后,再绘制到界面上。
https => 构建DOM树 => 计算css => 排版 => 渲染和合成 => 绘制
总体来说,整个过程就是:浏览器把URL(其中还有DNS解析)通过http请求/响应变成字符流,把字符流变成词(token)流,把词(token)流构造成DOM树,把不含样式信息的DOM树应用CSS规则,变成包含样式信息的DOM树,并且根据样式信息,计算了每个元素的位置和大小。最后就是根据这些样式信息和大小信息,为每个元素在内存中渲染它的图形,并且把它绘制到对应的位置。 下面就对其细节进行详细的分解:
-
HTTP协议
HTTP协议是基于TCP协议出现的,对TCP协议来说,TCP协议是一条双向的通讯通道,HTTP在TCP的基础上,规定了Request-Response的模式。这个模式决定了通讯必定是由浏览器端首先发起的。浏览器的实现者只需要用一个TCP库,甚至一个现成的HTTP库就可以搞定浏览器的网络通讯部分。HTTP是纯粹的文本协议,它是规定了使用TCP协议来传输文本格式的一个应用层协议。如下是请求和相应举例:
// request GET / HTTP/1.1 Host: time.geekbang.org // response HTTP/1.1 301 Moved Permanently Date: Fri, 25 Jan 2019 13:28:12 GMT Content-Type: text/html Content-Length: 182 Connection: keep-alive Location: https://test.abx.org/ Strict-Transport-Security: max-age=15768000 <html> <head><title>301 Moved Permanently</title></head> <body bgcolor="white"> <center><h1>301 Moved Permanently</h1></center> <hr><center>openresty</center> </body> </html>
在请求部分,第一行被称作 request line,它分为三个部分,HTTP Method,也就是请求的“方法”,请求的路径和请求的协议和版本。
在响应部分,第一行被称作 response line,它也分为三个部分,协议和版本、状态码和状态文本。
紧随在request line或者response line之后,是请求头/响应头,这些头由若干行组成,每行是用冒号分隔的名称和值。
在头之后,以一个空行(两个换行符)为分隔,是请求体/响应体,请求体可能包含文件或者表单数据,响应体则是html代码。
如下图所示:
在这些部分中,path是请求的路径完全由服务端来定义,没有很多的特别内容;而version几乎都是固定字符串;response body是我们最熟悉的HTML。
- HTTP Method(方法)
- GET
- POST
- HEAD
- PUT
- DELETE
- CONNECT
- OPTIONS
- TRACE
浏览器通过地址栏访问页面都是GET方法。表单提交产生POST方法。HEAD则是跟GET类似,只返回请求头,多数由JavaScript发起。PUT和DELETE分别表示添加资源和删除资源,但是实际上这只是语义上的一种约定,并没有强约束。CONNECT现在多用于HTTPS和WebSocket。OPTIONS和TRACE一般用于调试,多数线上服务都不支持。
- HTTP Status code(状态码)和 Status text(状态文本)
- 1xx:临时回应,表示客户端请继续。
- 2xx:请求成功。
- 200:请求成功。
- 3xx: 表示请求的目标有变化,希望客户端进一步处理。
- 301&302:永久性与临时性跳转(重定向)。
- 304:跟客户端缓存没有更新。
- 4xx:客户端请求错误。
- 403:无权限。
- 404:表示请求的页面不存在。
- 418:It’s a teapot. 这是一个彩蛋,来自ietf的一个愚人节玩笑。(超文本咖啡壶控制协议)
- 5xx:服务端请求错误。
- 500:服务端错误。
- 503:服务端暂时性错误,可以一会再试
- HTTP Head (HTTP头)
HTTP头可以看作一个键值对。原则上,HTTP头也是一种数据,我们可以自由定义HTTP头和值。不过在HTTP规范中,规定了一些特殊的HTTP头.如下:
- request header
- response header
-
HTTP Request Body
HTTP请求的body主要用于提交表单场景。实际上,http请求的body是比较自由的,只要浏览器端发送的body服务端认可就可以了。一些常见的body格式是:
- application/json
- application/x-www-form-urlencoded
- multipart/form-data
- text/xml
我们使用html的form标签提交产生的html请求,默认会产生 application/x-www-form-urlencoded 的数据格式,当有文件上传时,则会使用multipart/form-data。
- HTTPS和HTTP 2.0
在HTTP协议的基础上,HTTPS和HTTP2规定了更复杂的内容,但是它基本保持了HTTP的设计思想,即:使用上的Request-Response模式。
HTTPS有两个作用,一是确定请求的目标服务端身份,二是保证传输的数据不会被网络中间节点窃听或者篡改。HTTPS是使用加密通道来传输HTTP的内容。但是HTTPS首先与服务端建立一条TLS加密通道。TLS构建于TCP协议之上,它实际上是对传输的内容做一次加密,所以从传输内容上看,HTTPS跟HTTP没有任何区别。
HTTP 2.0 最大的改进有两点,一是支持服务端推送,二是支持TCP连接复用。服务端推送能够在客户端发送第一个请求到服务端时,提前把一部分内容推送给客户端,放入缓存当中,这可以避免客户端请求顺序带来的并行度不高,从而导致的性能问题。TCP连接复用,则使用同一个TCP连接来传输多个HTTP请求,避免了TCP连接建立时的三次握手开销,和初建TCP连接时传输窗口小的问题。
- HTTP Method(方法)
-
解析HTML代码和构建DOM树
- 解析
浏览器解析html这个过程,实际就是对html内容的编译。可以通过编译原理中的token来解释(表示最小的有意义的单元)
-
词(token)是如何被拆分的
<p class="a">text text text</p>
- <p “标签开始”的开始;
- class=“a” 属性;
-
“标签开始”的结束;
- text text text 文本;
- </p>标签结束。
HTML的结构不算太复杂,“词”的种类大约只有标签开始、属性、标签结束、注释、CDATA节点几种。
浏览器每解析一个字符就会不断进行判断,从而获取词token。从而不断将字符流解析成词。
- 状态机
绝大多数语言的词法部分都是用状态机实现的。用状态机做词法分析,其实正是把每个词的“特征字符”逐个拆开成独立状态,然后再把所有词的特征字符链合并起来,形成一个联通图结构。由于状态机设计属于编译原理的知识,这里不进行展开。
接下来就是代码实现的事情了,在C/C++和JavaScript中,实现状态机的方式大同小异:我们把每个函数当做一个状态,参数是接受的字符,返回值是下一个状态函数。
-
-
构建DOM树
接下来我们要把这些简单的词变成DOM树,这个过程我们是使用栈来实现的,任何语言几乎都有栈,如下是js中的栈
function HTMLSyntaticalParser(){ var stack = [new HTMLDocument]; this.receiveInput = function(token) { //…… } this.getOutput = function(){ return stack[0]; } }
我们这样来设计HTML的语法分析器,receiveInput负责接收词法部分产生的词(token),通常可以由emmitToken来调用。
在接收的同时,即开始构建DOM树,所以我们的主要构建DOM树的算法,就写在receiveInput当中。当接收完所有输入,栈顶就是最后的根节点,我们DOM树的产出,就是这个stack的第一项。
如下示例解析过程:
<html maaa=a > <head> <title>cool</title> </head> <body> <img src="a" /> </body> </html>
- 栈顶元素就是当前节点;
- 遇到属性,就添加到当前节点;
- 遇到文本节点,如果当前节点是文本节点,则跟文本节点合并,否则入栈成为当前节点的子节点;
- 遇到注释节点,作为当前节点的子节点;
- 遇到tag start就入栈一个节点,当前节点就是这个节点的父节点;
- 遇到tag end就出栈一个节点(还可以检查是否匹配)。
- 解析
- CSS计算
首先CSS需要先经过词法分析和语法分析,转变成计算机能够理解的结构。即CSS先被解析成一个可用的语法抽象树。
其次在构建元素的过程中,对每一个构建好的元素,去检查它匹配的css规则,再依据规则的优先级,最覆盖和调整。这样就给DOM元素添加上了css属性。
-
排版
浏览器最基本的排版方案是正常流排版,它包含了顺次排布和折行等规则,这是一个跟我们提到的印刷排版类似的排版方案,也跟我们平时书写文字的方式一致,所以我们把它叫做正常流。
浏览器又可以支持元素和文字的混排,元素被定义为占据长方形的区域,还允许边框、边距和留白,这个就是所谓的盒模型。
在正常流的基础上,浏览器还支持两类元素:绝对定位元素和浮动元素。
-
绝对定位元素把自身从正常流抽出,直接由top和left等属性确定自身的位置,不参加排版计算,也不影响其它元素。绝对定位元素由position属性控制。
-
浮动元素则是使得自己在正常流的位置向左或者向右移动到边界,并且占据一块排版空间。浮动元素由float属性控制。
除了正常流,浏览器还支持其它排版方式,比如现在非常常用的flex排版,这些排版方式由外部元素的display属性来控制(注意,display同时还控制元素在正常流中属于inline等级还是block等级)。
-
- 渲染
- 渲染
在计算机图形学领域里,英文render(渲染)这个词是一个简写,它是特指把模型变成位图的过程。这里的位图就是在内存里建立一张二维表格,把一张图片的每个像素对应的颜色保存进去(位图信息也是DOM树中占据浏览器内存最多的信息,我们在做内存占用优化时,主要就是考虑这一部分)。
浏览器中渲染这个过程,就是把每一个元素对应的盒变成位图。这里的元素包括HTML元素和伪元素,一个元素可能对应多个盒(比如inline元素,可能会分成多行)。每一个盒对应着一张位图。
- 合成
合成是英文术语compositing的翻译,这个过程实际上是一个性能考量,它并非实现浏览器的必要一环。合成是一个性能考量,那么合成的目标就是提高性能,根据这个目标,我们建立的原则就是最大限度减少绘制次数原则。
- 绘制
绘制是把“位图最终绘制到屏幕上,变成肉眼可见的图像”的过程,不过,一般来说,浏览器并不需要用代码来处理这个过程,浏览器只需要把最终要显示的位图交给操作系统即可。当绘制完成时,就完成了浏览器的最终任务,把一个URL最后变成了一个可以看的网页图像。
- 渲染