"描述一下在浏览器中从输入 URL 到页面显示的过程" 作为一道经典面试题,因为涉及的知识范围比较广,考察点比较多,所以无论是校招,还是社招,都会被频繁问到,下面我们来一起梳理一下整个过程是什么样的以及中间涉及到的一些知识点。
主要过程如下:
- URL 解析和 DNS 查询(获取 IP 地址);
- TCP 三次握手,发起 HTTP 请求;
- 服务器处理请求,HTTP 响应请求;
- 页面渲染,包括解析 HTML+CSS+JS、布局、绘制、合成;
- 断开连接:TCP 四次挥手。
一、URL 解析和 DNS 查询(获取 IP 地址)
当用户在浏览器地址栏中输入一个 URL 时,
- 首先,浏览器会对 URL 进行解析,包括检查 URL 的格式、协议、主机名等等,确定要访问的站点地址;
- 然后去浏览器缓存中查找,如果不存在缓存,就进行 DNS 查询获取对应的 IP 地址。
URL 的组成
以https://www.runoob.com/css/css-tutorial.html
为例
- 协议:指该计算机获取资源的方式,一般有 HTTP、HTTPS、FTP 等协议,不同协议有不同的通讯内容格式。其中常用的就是 HTTP、HTTPS。
- 网络地址:指连接网络上哪台计算机或服务器,可以是域名或 IP 地址,后面还可能带冒号和端口号,一般端口号都是默认的,比如 HTTP 的默认端口号是 80,HTTPS 的默认端口号是 443。如果使用默认端口号,则 URL 上不需要带上端口号,否则需要带上。
- 资源路径 :指从服务器上获取哪一项资源的等级路径,用
/
分割。 - 文件名 :指真正需要访问的文件名,有的 URL 直接以
/
为结尾,则表示访问的是最后一层文件路径下的index.html
或default.html
文件。 - 动态参数 :指以
?
开始的参数,一般是需要传给服务端进行动态查询的。
浏览器缓存
浏览器缓存位置
浏览器缓存从缓存位置上来说可分为四种,并且各自有优先级,当依次查找缓存没有命中的时候,才会去请求网络。
-
Service Worker
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。
使用 Service Worker 的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,需要 HTTPS 协议来保障安全。
Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制 缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持久性的 。
Service Worker 实现缓存功能的步骤:
- 首先注册 Service Worker;
- 然后监听到 install 事件后就可以缓存需要的文件;
- 那么在下次用户访问的时候,可以通过拦截请求的方式查询是否存在缓存,存在缓存的话,就可以直接读取缓存文件,否则就去请求数据。
-
Memory Cache
Memory Cache 就是内存中的缓存,读取内存中的数据肯定比读取磁盘快。
内存缓存虽然读取高效,但是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 Tab 页,内存中的缓存也被释放了。
Memory Cache 会将编译解析后的文件直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。一般 JS、字体、图片等会放在内存缓存中。 -
Disk Cache
Disk Cache 是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比 Memory Cache 胜在容量和存储时效上 。
在所有浏览器缓存中,Disk Cache 的覆盖面基本上是最大的。它会根据 HTTP Header 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。
Disk Cache 直接将缓存写入硬盘文件中,读取缓存需要对缓存存放的硬盘文件进行 I/O 操作,然后重新解析该缓存中的内容,读取复杂,速度慢。一般 CSS 会放在硬盘缓存中。 -
Push Cache
Push Cache(推送缓存)是 HTTP/2 中的内容。当以上缓存都没有被命中的时候,它才会被使用。它只在会话(session)中存在,一旦会话结束就被释放,并且缓存时间也很短,在 Chrome 浏览器中只有 5 分钟左右 。
它有如下一些特性:
- 所有的资源都能被推送,并且能够被缓存,但是 Edge 和 Safir 浏览器兼容性不怎么好;
- 可以推送
no-cache
和no-store
的资源; - 一旦连接被关闭,Push Cache 就会被释放;
- 多个页面可以使用相同的 HTTP/2 连接,也就是说能使用同样的缓存;
- Push Cache 中的缓存只能被使用一次;
- 浏览器可以拒绝接受已经存在的资源推送;
- 可以给其他域名推送资源。
浏览器缓存策略(HTTP缓存机制)
浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置HTTP Header
来实现的。
-
强缓存
浏览器在请求资源时会先检查本地缓存,如果存在且未过期,则直接使用本地缓存,不发起请求到服务器。状态码为 200。
强缓存可以通过设置以下两种
HTTP Header
实现:-
Expires
Expires
是 HTTP/1 的产物,表示资源会在某个时间后过期,需要再次请求。
Expires
受限于本地时间,如果修改了本地时间,可能会造成缓存失效。yamlExpires: Wed, 22 Oct 2023 08:41:00 GMT #表示资源会在 `Wed, 22 Oct 2023 08:41:00 GMT` 后过期。
-
Cache-Control
Cache-Control
出现于 HTTP/1.1,优先级高于 Expires。
Cache-Control
是一个通用类消息头字段,可以在请求头或响应头中设置,并且可以组合使用多种指令,通过指定指令来实现缓存机制。常见指令 作用 public(响应指令) 表示响应可以被任何对象缓存(即使是通常不能被缓存的内容) private(响应指令) 表示响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存)。 no-cache(请求/响应指令) 表示在发布缓存副本之前,强制要求缓存把请求交给原始服务器进行验证(协商缓存验证)。 max-age=< seconds >(请求/响应指令) 设置缓存存储的最大周期(单位秒),时间是相对请求的时间。 yamlCache-Control: max-age=30 #表示资源会在30秒后过期
-
-
协商缓存
如果强缓存失效了,就需要发起请求验证资源是否有更新。当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态码(Not Modified),并且更新浏览器缓存有效期。
协商缓存可以通过设置以下两种
HTTP Header
实现:Last-Modified
响应 Header 和If-Modified-Since
请求 Header
当浏览器发起请求时,服务器会返回资源的最后修改时间(Last-Modified
),然后浏览器在下一次请求时会发送If-Modified-Since
头,询问服务器在该时间后资源是否有更新,有更新的话,就将新的资源发送回来,否则返回 304 状态码。
但是Last-Modified
有一些弊端:- 如果本地打开缓存文件,就算没有对文件进行更改,但还是会造成
Last-Modified
被更改,服务器不能命中缓存,导致发送相同的资源。 - 因为
Last-Modified
只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务器会认为资源还是命中了,不会返回正确的资源。
基于上述问题,所以在 HTTP/1.1 中出现了 ETag。
- 如果本地打开缓存文件,就算没有对文件进行更改,但还是会造成
ETag
响应 Header 和If-None-Match
请求 Header
ETag
是服务器生成的资源唯一标志符,当资源发生变化时,ETag
也会发生变化。
浏览器在请求时发送If-None-Match
头,询问该资源ETag
是否变动,有变动的话就将新的资源发送回来,否则返回 304 状态码。
ETag
的优先级比Last-Modified
高。
如果什么缓存策略都没有设置,那么浏览器会怎么处理?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头的Date
减去Last-Modified
值的 10% 作为缓存时间。
用户行为
- 地址栏访问:链接跳转是正常用户行为,会触发浏览器缓存机制;
- F5 刷新 :浏览器会设置
max-age=0
,跳过强缓存判断,直接进行协商缓存判断; - ctrl+F5 刷新:跳过强缓存和协商缓存,直接从服务器拉取资源。
三级缓存原理(访问缓存优先级)
- 先在内存中查找,如果有,直接加载;
- 如果内存中不存在,则在硬盘中查找,如果有直接加载;
- 如果硬盘中也没有,那么就进行网络请求;
- 请求获取的资源缓存在硬盘和内存中。
内存缓存和硬盘缓存有什么区别?
区别 | 内存缓存(Memory Cache) | 硬盘缓存(Disk Cache) |
---|---|---|
存储内容 | JS、字体、图片等 | CSS 等 |
读取速度 | 快 | 慢 |
时效性 | 进程关闭则清空 | 可以缓存较长时间 |
空间 | 空间小 | 空间大 |
浏览器的缓存存放在哪里,如何在浏览器中判断强缓存是否生效?
当命中强缓存时状态码为200,请求对应的 size 值则代表该缓存存放的位置,分别为 from disk cache 和 from memory cache。
from disk cache 代表使用硬盘中的缓存,from memory cache 代表使用内存中的缓存。
浏览器读取缓存的顺序为 memory > cache。
为什么 CSS 会存放在硬盘缓存中?
因为 CSS 文件加载一次就会被渲染出来,我们不会频繁读取它,所以它不适合缓存到内存中。但 JS 之类的脚本却随时可能会被执行,如果脚本在磁盘中,我们在执行脚本的时候需要从磁盘取搭配内存中来,这样 IO 开销就很大了,可能会导致浏览器失去响应。
HTTP 常见状态码
HTTP 状态码(HTTP Status Code)是用来表示网页服务器超文本传输协议响应状态的3位数字代码。
它由 RFC 2616 规范定义,并得到 RFC 2518、RFC 2817、RFC 2295、RFC 2774 与 RFC 4918 等规范扩展。
简单来说,HTTP 状态码是服务器告诉客户端当前请求响应的状态,通过状态码可以判断和分析服务器的运行状态。
-
1XX,信息类:服务器收到请求,需要请求者继续执行操作
状态码 英文名称 中文描述 100 Continue 继续,客户端应继续其请求。 101 Switching Protocols 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,主要用于 websocket 或 http2 升级。例如,切换到 HTTP 的新版本协议 -
2XX,成功类:操作被成功接收并处理
状态码 英文名称 中文描述 200 OK 请求成功,一般用于 GET 与 POST 请求。 201 Created 已创建。成功请求并且服务器创建了新的资源 202 Accepted 已接受。服务器已经接受请求,但未处理完成 203 Non-Authoritative Information 非授权信息。服务器已经成功处理请求,但返回的信息可能来自另一来源。 204 No Content 无内容。服务器成功处理,但未返回内容。 205 Reset Content 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域 206 Partial Content 部分内容。服务器成功处理了部分 GET 请求 -
3XX,重定向类:需要进一步的操作以完成请求
状态码 英文名称 中文描述 300 Multiple Choices 多种选择。服务器可执行多种操作。服务器根据请求者选择一项操作,或提供操作列表供请求者选择。 301 Moved Permanently 永久移动。请求的网页已经永久移动到新位置,服务器返回此相应时,会自动将请求者转到新位置。 302 Found 临时移动。服务器目前从不同位置的网页访问请求,但请求者应继续使用原有位置来进行以后的请求。 303 See Other 查看其它地址。与 301 类似。使用 GET 和 POST 请求查看 304 Not Modified 未修改,所请求的资源未修改。服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改过的资源。 305 Use Proxy 使用代理。所请求的资源必须通过代理访问 307 Temporary Redirect 临时重定向。与 302 类似。使用 GET 请求重定向 -
4XX,客户端错误类:请求包含语法错误或无法完成请求
状态码 英文名称 中文描述 400 Bad Request 错误请求。客户端请求的语法错误,服务器无法理解。 401 Unauthorized 未授权。请求要求用户的身份认证(对于需要登陆的网页,服务器可能返回此响应)。 403 Forbidden 禁止。服务器理解客户端的请求,但是拒绝执行该请求。 404 Not Found 未找到。服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面。 405 Method Not Allowed 方法禁用。客户端请求中的方法被禁止 406 Not Acceptable 不接受。服务器无法根据客户端请求的内容特性完成请求 407 Proxy Authentication Required 请求要求代理的身份认证。与 401 类似,但请求者应当使用代理进行授权 408 Request Time-out 超时。服务器等待客户端发送的请求时间过长。 -
5XX,服务器错误类:服务器在处理请求的过程中发生了错误
状态码 英文名称 中文描述 500 Internal Error 服务器内部错误,无法完成请求。 502 Bad Gateway 错误网关。作为网关或代理的服务器尝试执行请求,从远程服务器接收到了一个无效的响应。 504 Gateway Timeout 网关超时。充当网关或代理的服务器,未及时从远端服务器获取请求。
DNS查询
DNS(Domain Name System,域名系统),是互联网的一项服务,是进行域名和与之相对应的 IP 地址进行转换的服务器。
域名是一个具有层次的结构,从上到下依次为根域名、顶级域名、二级域名、三级域名...
例如:www.baidu.com
,www
为三级域名,baidu
为二级域名,com
为顶级域名。系统为用户做了兼容,域名末尾的根域名.
一般不需要输入。
在域名的每一层都会有一个域名服务器,包括根域名服务器、顶级域名服务器以及权限域名服务器。
此外,还有电脑默认的本地域名服务器。
DNS 查询方式:
- 递归查询:如果A请求B,那么B作为请求的接收者一定要给A想要的答案。
- 迭代查询:如果接收者B没有请求者A所需要的准确内容,接收者B将告诉请求者A如何去获取这个内容,但自己并不发送请求。
域名缓存方式:
- 浏览器缓存:浏览器在获取网站域名的实际IP地址后会对其进行缓存,减少网络请求的损耗。
- 操作系统缓存 :操作系统的缓存其实是用户自己配置的
hosts
文件
DNS 查询过程:
- 首先搜索浏览器的 DNS 缓存,缓存中维护一张域名与IP地址的对应表;
- 若没有命中,则继续搜索操作系统的 DNS 缓存;
- 若仍然没有命中,则域名系统将域名发送至本地域名服务器,本地域名服务器采用递归查询自己的 DNS 缓存,查找成功则返回结果;
- 若本地域名服务器的 DNS 缓存没有命中,则本地域名服务器向上级域名服务器进行迭代查询。
- 首先本地域名服务器向根域名发起请求,根域名服务器返回顶级域名服务器的地址给本地域名服务器;
- 本地域名服务器拿到这个顶级域名服务器的地址后,就向其发起请求,获取权限域名服务器的地址;
- 本地域名服务器根据权限域名服务器的地址向其发起请求,最终得到该域名对应的 IP 地址。
- 本地域名服务器将得到的 IP 地址返回给操作系统,同时自己将 IP 地址缓存起来;
- 操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起来;
- 至此,浏览器就得到了域名对应的 IP 地址,并将 IP 地址缓存起来。
二、TCP 连接和 HTTP 请求
通过 TCP 三次握手建立 TCP 连接。当 TCP 连接建立后,浏览器会发送 HTTP 请求到服务器。
TCP 三次握手的过程
一开始,客户端和服务端处于CLOSED
状态。客户端主动打开连接,服务端被动打开连接,结束CLOSED
状态,开始监听,进入LISTEN
状态。
-
第一次握手(由客户端发起)
客户端会随机初始化序号(
client_isn
),将此序号置于 TCP 首部的序号字段中,同时把SYN
标识位置为1
,表示SYN
报文;接着把第一个SYN
报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据;之后客户端处于SYN_SENT
状态。 -
第二次握手(由服务端发起)
服务端收到客户端的
SYN
报文后,首先服务端也初始化自己的序号(server_isn
),将此序号填入 TCP 首部的序号字段中;其次把 TCP 首部的确认应答号(Ack Num
)字段填入client_isn + 1
,接着把SYN
和ACK
标识位置为1
;最后把该报文发送给客户端,该报文也不包含应用层数据;之后服务端处于SYN_RCVD
状态。 -
第三次握手(由客户端发起)
客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部
ACK
标识位置为1
;其次确认应答号(Ack Num
)字段中填入server_isn + 1
;最后把报文发送给服务端;这次报文可以携带应用层数据,之后客户端和服务端都处于ESTABLISHED
状态。
三次握手的目的 / 为什么不是两次握手?
- 防止已失效的连接请求报文突然又传送到了服务端,因而产生错误。
- 为了防止服务端开启一些无用的连接,增加服务器开销。
- 如果只有两次握手,发送端可以确认自己发送的信息对方能收到,也可以确认对方发送的信息自己能收到,但是接收端无法确认自己发送的信息对方是否能收到。因此,需要三次握手来确认双方的接收和发送能力都正常。
HTTP 请求
HTTP 请求包括请求行、请求头和请求体。
- 请求行:包括请求方法(POST、GET等)和路径信息
- 请求头:包括一些额外的信息,例如浏览器的 User-Agent、Accept-Encodeing 等。
- 请求体:包括一些提交的数据,例如表单数据或 JSON 数据等。
跨域
跨域指的是在 Web 开发中,由于浏览器的同源策略限制,当一个请求 url 的协议、域名、端口号任意一个和当前页面 url 不同,就都会产生跨域。
跨域并不是请求发送不出去,请求能发出去,服务端能接收并正常返回结果,但是结果被浏览器拦截了。
如何解决跨域
-
JSONP
利用
<script>
标签没有跨域限制的漏洞。但仅限于 GET 请求,不安全,可能会遭受 XSS 攻击。
实现流程:- Web 前端事先定义一个用于获取跨域响应数据的回调函数;
- 通过没有同源策略限制的
<script>
标签发起一个请求(将回调函数的名称放到这个请求的 query 参数里); - 服务端返回这个回调函数的执行,并将需要响应的数据放到回调函数的参数里;
- 前端的 script 标签请求到这个执行的回调函数后会立即执行,于是就拿到了执行的响应数据。
html<script src="http://domain/api?param1=a¶m2=b&callback=show"></script> <script> function show(data) { console.log(data); } </script>
js// server.js let express = require('express'); let app = express(); app.get('/say', function(req, res) { let { name, callback } = req; console.log(name); // zhangsan console.log(callback); // show res.end(`${callback}('我叫张三')`); }) app.listen();
实现一个 JSONP:
jsfunction show(data) { console.log(data); } function jsonp({url, params, callback}) { return new Promise((resolve, reject) => { let script = document.createElement('script'); window[callback] = function(data) { resolve(data); document.body.removeChild(script); } params = {...params, callback}; let arr = []; for(let key in params) { arr.push(`${key}=${params[key]}`); } script.src = `${url}?${arr.join('&')}`; document.body.appendChild(script); }) } jsonp({ url: 'http://localhost:3000/say', params: { name: 'zhangsan' }, callback: 'show' }).then(data => { console.log(data); })
-
CORS(Cross-origin resource sharing,跨域资源共享)
CORS 需要浏览器和服务端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是服务端。只要服务端实现了 CORS,就实现了跨域。
服务端设置
Access-Control-Allow-Origin
就可以开启 CORS。该属性表示那些域名可以访问资源,如果设置通配符,则表示所有网站都可以访问资源。浏览器请求分为简单请求和复杂请求:
-
简单请求
当满足以下条件时,就会触发简单请求:- 使用
GET
、HEAD
、POST
这三种方法中的其中一种 Content-Type
(实体头部)的值仅限于text/plain
、mutipart/form-data
、application/x-www-form-urlencoded
这三种
- 使用
-
复杂请求
不满足以上条件的就是复杂请求。
对于复杂请求来说,会首先发起一个预检请求,该请求是 Option 方法,用于查询服务器所支持的 CORS 安全头和方法,并确认客户端请求是否被服务器允许。
如何减少 options 请求的次数?jsAccess-Control-Max-Age: 3600
-
-
postMessage
postMessge()
是 HTML5 XMLHTTPRequest Level2 中的 API,允许来自不同源的脚本采用异步方式进行有限的通讯,可以实现多窗口、跨域消息传递。
window.postMessage(message, targetOrigin, [transfer])
html<!--a.html--> <iframe src="http:_localhost:4000_b" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件 <!--内嵌在http://localhost:3000/a.html--> <script> function load() { let frame = document.getElementById('frame'); frame.contentWindow.postMessage('你好吗?', 'http://localhost:4000'); //发送数据 window.onmessage = function(e) { // 接受返回数据 console.log(e.data) // 我很好 }; } </script> <!--b.html--> <script> window.onmessage = function(e) { console.log(e.data); // 你好吗? e.source.postMessage('我很好', e.origin); } </script>
-
WebSocket
WebSocket 是 HTML5 的一个持久化协议,它实现了浏览器和服务器的全双工通信,同时也是跨域的一种解决方式。在建立连接之后,WebSocket 的 server 和 client 都能主动向对方发送和接收数据。
-
Node 中间件代理(两次跨域)
实现原理:同源策略是浏览器要遵循的标准,而如果是服务器向服务器请求,则不需要遵循同源标准。
代理服务器需要做以下几个步骤:
- 接收客户端请求;
- 将请求转发给服务器;
- 拿到服务器响应数据;
- 将响应转发给客户端。
- Nginx 反向代理
实现原理类似于 Node 中间件代理原理,需要搭建一个中转 Nginx 服务器,用于转发请求。
使用 Nginx 反向代理实现跨域,是最简单的跨域方式,只要修改 Nginx 配置,即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器性能。
三、服务器处理和 HTTP 响应
服务器收到 HTTP 请求后,会根据请求的内容进行处理。服务器可能会读取请求体中的数据,查询数据库,执行业务逻辑等操作。
处理完成后,服务器会生成 HTTP 响应并发送回浏览器。
HTTP 响应
HTTP 响应包括状态行、响应头和响应体。
- 状态行:包括状态码,表示服务器处理请求的结果。
- 响应头:包含一些附加信息,如服务器类型、响应时间等。
- 响应体:包含实际返回的数据,如 HTML 页面、JSON 数据等。
四、页面渲染
浏览器收到服务器的响应后,会对返回的资源进行解析和渲染。如果响应是 HTML 页面,浏览器会构建 DOM 树和 CSS 规则树,并最终生成渲染树。然后浏览器会对渲染树进行布局和绘制,最终将页面呈现给用户。
在页面渲染的过程中,浏览器还会下载页面中引用的其他资源,例如 CSS 文件、JavaScript 文件、图片等,并对这些资源进行相同的解析和渲染过程。
解析 HTML
浏览器从服务端接收 HTML 文档后会进行解析,构建 DOM 树(文件对象模型)。DOM 树是文档的逻辑结果表示,每个 HTML 标签都会被解析成 DOM 树的一个节点,标签之间的嵌套关系则对应着 DOM 树中的父子关系。
如何避免script
标签阻塞页面渲染
- 异步加载脚本:将
script
标签的async
或defer
属性设置为true
,注意只对外部脚本文件有效- 什么都没设置:HTML 暂停解析,下载 JS,执行 JS,再继续解析 HTML。
- defer:HTML 继续解析,并行下载 JS,等待 HTML 解析完之后,执行 JS。
- async:HTML 继续解析,并行下载 JS,JS 下载完后立即执行,HTML 暂停解析,等待 JS 执行完后,HTML 继续解析。
- 将脚本放在页面底部(
</body>
前),但是如果脚本需要在页面加载期间执行,该方法则不适用。 - 优化脚本内容:确保脚本内容本身也是高效执行的。
解析 CSS
解析 CSS 样式表,生成 CSS 规则树(样式表对象模型)。CSS 规则树描述了文档中各个元素应用的样式信息。DOM 树和 CSS 规则树结合后形成了渲染树(Render Tree),渲染树只包含需要显示的节点和其对应的样式信息。
JS 和 CSS 是如何影响 DOM 树构建的?
CSS 不会阻塞 DOM 的解析,但是会影响 JavaScript 的运行(比如通过控制元素的可见性visibility/display
,影响 js 对元素的操作),JavaScript 会阻止 DOM 树的解析,最终 CSS(CSSOM)会影响 DOM 树的渲染,也可以说最终会影响渲染树的生成。
-
JavaScript 脚本在 HTML 页面中
当解析到 script 脚本标签时,HTML 解析器暂停工作,js 引擎介入,并执行 script 标签中的脚本。
因为下面这段 JavaScript 脚本修改了 DOM 中第一个 div 中的内容,所以执行这段脚本之后,div 节点内容已经修改为
time.geekbang
了。脚本执行完成之后,HTML 解析器恢复解析过程,继续解析后续的内容,直至生成最终的 DOM。html<html> <body> <div>1</div> <script> let div1 = document.getElementsByTagName('div')[0]; div1.innerText = '你好'; </script> <div>test</div> </body> </html>
-
HTML 页面引入 JS 文件
其整个执行流程还是一样的,执行到 JavaScript 标签时,暂停整个 DOM 的解析,执行 JavaScript 代码,不过这里执行 JavaScript 时,需要下载这段代码。这里需要注意下载环境,因为 JavaScript 文件的下载过程会阻塞 DOM 解析,而通常下载又是非常耗时的,会受到网络环境、JavaScript 文件大小等因素的影响。
js// foo.js let div1 = document.getElementsByTagName('div')[0] div1.innerText = 'time.geekbang'
HTML<html> <body> <div>1</div> <script type="text/javascript" src='foo.js'></script> <div>test</div> </body> </html>
-
HTML 页面中有 CSS 文件
下面示例中,JavaScript 代码出现了
div1.style.color = 'red'
的语句,它是用来操纵 CSSOM 的,所以在执行 JavaScript 之前,需要先解析 JavaScript 语句之上所有的 CSS 样式。所以如果代码里引用了外部的 CSS 文件,那么在执行 JavaScript 之前,还需要等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行 JavaScript 脚本。而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载、解析操作,再执行 JavaScript 脚本。所以说 JavaScript 脚本是依赖样式表的,这又多了一个阻塞过程。
css/* theme.css */ div {color:blue}
html<html> <head> <style src='theme.css'></style> </head> <body> <div>1</div> <script> let div1 = document.getElementsByTagName('div')[0] div1.innerText = 'time.geekbang' // 需要 DOM div1.style.color = 'red' // 需要 CSSOM </script> <div>test</div> </body> </html>
布局(Layout)
渲染树中的每个节点都有对应的几何信息,例如位置、大小等。布局阶段会根据渲染树中每个节点的几何信息计算节点在页面中的精确位置,确定每个元素在屏幕上的显示位置。
绘制(Paint)
在布局完成后,浏览器将根据渲染树和布局阶段得到的几何信息来绘制页面。这个过程将把页面的每个节点转换为屏幕上的实际像素,创建一个包含所有像素信息的位图。
回流和重绘
回流必将引起重绘,重绘不一定会引起回流。
-
回流(reflow)
当渲染树中部分或全部元素的几何属性(尺寸、结构或某些属性)发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:- 页面首次渲染
- 浏览器窗口大小发生变化
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或删除可见的 DOM 元素
- 激活 css 伪类(例如:hover)
- 查询某些属性或调用某些方法
一些常用但会导致回流的属性和方法:
clientWidth
、clientHeight
、clientTop
、clientLeft
offsetWidth
、offsetHeight
、offsetTop
、offsetLeft
scrollWidth
、scrollHeight
、scrollTop
、scrollLeft
scrollIntoView()
、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
-
重绘(repaint)
当页面中元素样式的改变并不影响它在文档流中的位置时(如:color
、background-color
、visibility
等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
如何减少回流和重绘?
- CSS
- 避免使用
table
布局 - 尽可能在 DOM 树的最末端改变 class
- 避免设置多层内联样式
- 将动画效果应用到
position
属性为absolute
或fixed
的元素上,使其尽可能脱离文档流,从而减少对其他元素的影响 - 避免使用 css 表达式
- 优化动画,使用 CSS3 和 requestAnimationFrame
- 避免使用
- JS
- 避免频繁操作样式,最好一次性重写 style 属性,或将样式列表定义为 class 并一次性更改 class 属性
- 避免重复操作 DOM,创建一个
documentFragment
,在它上面应用所有 DOM 操作,最后再把它添加到文档中:因为documentFragement
不是真实的 dom 部分,所以不会引起回流和重绘。 - 可以先为元素设置
display:none
,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘 - 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
CSS3 动画会引起回流和重绘吗?
CSS3 动画在浏览器中的实现通常是通过 GPU 加速的方式来提高性能,因此它不会引起回流,但会引起重绘。
合成(Composite)
绘制阶段完成后,浏览器会将各个图层(Layer)按照合成顺序进行合并,形成最终的页面图像。合成是将各个图层的像素信息进行组合,以最终形成用户所见的页面。
五、断开连接
TCP 四次挥手
-
第一次挥手(客户端发起)
客户端打算关闭连接,此时会发送一个 TCP 首部
FIN
标识位被置为1
的报文,即FIN
报文;之后客户端进入FIN_WAIT_1
状态。 -
第二次挥手(服务端发起)
服务端收到
FIN
报文后,就向客户端发送ACK
应答报文;之后服务端进入CLOSED_WAIT
状态;客户端收到服务端发送的ACK
应答报文后,进入FIN_WAIT_2
状态。 -
第三次挥手(服务端发起)
等服务端处理完数据后向客户端发送
FIN
报文;之后服务端进入LAST_ACK
状态。 -
第四次挥手(客户端发起)
客户端收到服务端的
FIN
报文后,返回给服务端一个ACK
应答报文,之后客户端进入一个TIME_WAIT
状态;服务端接收到ACK
应答报文后,就进入了CLOSED
状态,至此服务端完成连接的关闭;客户端在经过2MSL
一段时间后,自动进入CLOSED
状态,至此客户端也完成连接的关闭。
为什么需要四次挥手?
- 关闭连接时,客户端向服务端发送
FIN
报文时,仅仅表示客户端不再发送数据了但是还能接收数据; - 服务器收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接; - 从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的
ACK
和FIN
一般都会分开发送,从而比三次握手导致多了一次。
为什么客户端在 TIME-WAIT 阶段要等 2MSL?
当客户端在第四次挥手发出最后的ACK
应答报文时,并不能确定服务端是否能够收到该段报文,所以客户端在发送完ACK
应答报文之后,会设置一个时长为 2MSL 的计时器。
MSL
指的是Maximum Segment Lifetime
:一段 TCP 报文在传输过程中的最大生命周期。他是任何豹纹在网络上存活的最长时间,超过这个时间报文将被丢弃。
- 服务器端在
1MSL
内没有收到客户端发出的ACK
应答报文,就会再次向客户端发出FIN
报文;- 如果客户端在
2MSL
内,再次收到了来自服务器端的FIN
报文,说明服务器端由于各种原因没有接收到客户端发出的ACK
应答报文;客户端再次向服务器端发出ACK
应答报文,计时器重置,重新开始 2MSL 的计时; - 否则客户端在
2MSL
内没有再次收到来自服务器端的FIN
报文,说明服务器端正常接收了ACK
应答报文,客户端可以进入CLOSED
阶段,完成"四次挥手"。
- 如果客户端在
所以,客户端要经历时长为2SML
的TIME-WAIT
阶段,以保证客户端最后一次挥手的报文能够到达服务器。
如有错误,欢迎指正~~