ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] # 浏览器输入 URL 后发生了什么? ## 解析URL 浏览器通过 URL 能够知道下面的信息: * Protocol "http" 使用HTTP协议 * Resource "/" 请求的资源是主页(index) ## 输入的是 URL 还是搜索的关键字? 当协议或主机名不合法时,浏览器会将地址栏中输入的文字传给默认的搜索引擎。大部分情况下,在把文字传递给搜索引擎的时候,URL会带有特定的一串字符,用来告诉搜索引擎这次搜索来自这个特定浏览器。 ## 转换非 ASCII 的 Unicode 字符 * 浏览器检查输入是否含有不是 a-z, A-Z,0-9, - 或者 . 的字符 * 这里主机名是 google.com ,所以没有非ASCII的字符;如果有的话,浏览器会对主机名部分使用 Punycode 编码 ## 检查 HSTS 列表 浏览器检查自带的“预加载 HSTS(HTTP严格传输安全)”列表,这个列表里包含了那些请求浏览器只使用HTTPS进行连接的网站 如果网站在这个列表里,浏览器会使用 HTTPS 而不是 HTTP 协议,否则,最初的请求会使用HTTP协议发送 注意,一个网站哪怕不在 HSTS 列表里,也可以要求浏览器对自己使用 HSTS 政策进行访问。浏览器向网站发出第一个 HTTP 请求之后,网站会返回浏览器一个响应,请求浏览器只使用 HTTPS 发送请求。然而,就是这第一个 HTTP 请求,却可能会使用户受到 downgrade attack 的威胁,这也是为什么现代浏览器都预置了 HSTS 列表。 ## DNS 查询 * 浏览器检查域名是否在缓存当中(要查看 Chrome 当中的缓存, 打开 chrome://net-internals/#dns)。 * 如果缓存中没有,就去调用 gethostbyname 库函数(操作系统不同函数也不同)进行查询。 * gethostbyname 函数在试图进行DNS解析之前首先检查域名是否在本地 Hosts 里,Hosts 的位置 不同的操作系统有所不同 * 如果 gethostbyname 没有这个域名的缓存记录,也没有在 hosts 里找到,它将会向 DNS 服务器发送一条 DNS 查询请求。DNS 服务器是由网络通信栈提供的,通常是本地路由器或者 ISP 的缓存 DNS 服务器。 * 查询本地 DNS 服务器 * 如果 DNS 服务器和我们的主机在同一个子网内,系统会按照下面的 ARP 过程对 DNS 服务器进行 ARP查询 * 如果 DNS 服务器和我们的主机在不同的子网,系统会按照下面的 ARP 过程对默认网关进行查询 ## ARP 过程 要想发送 ARP(地址解析协议)广播,我们需要有一个目标 IP 地址,同时还需要知道用于发送 ARP 广播的接口的 MAC 地址。 * 首先查询 ARP 缓存,如果缓存命中,我们返回结果:目标 IP = MAC 如果缓存没有命中: * 查看路由表,看看目标 IP 地址是不是在本地路由表中的某个子网内。是的话,使用跟那个子网相连的接口,否则使用与默认网关相连的接口。 * 查询选择的网络接口的 MAC 地址 * 我们发送一个二层( OSI 模型 中的数据链路层)ARP 请求: `ARP Request:` ~~~ Sender MAC: interface:mac:address:here Sender IP: interface.ip.goes.here Target MAC: FF:FF:FF:FF:FF:FF (Broadcast) Target IP: target.ip.goes.here ~~~ 根据连接主机和路由器的硬件类型不同,可以分为以下几种情况: 直连: * 如果我们和路由器是直接连接的,路由器会返回一个 ARP Reply (见下面)。 集线器: * 如果我们连接到一个集线器,集线器会把 ARP 请求向所有其它端口广播,如果路由器也“连接”在其中,它会返回一个 ARP Reply 。 交换机: * 如果我们连接到了一个交换机,交换机会检查本地 CAM/MAC 表,看看哪个端口有我们要找的那个 MAC 地址,如果没有找到,交换机会向所有其它端口广播这个 ARP 请求。 * 如果交换机的 MAC/CAM 表中有对应的条目,交换机会向有我们想要查询的 MAC 地址的那个端口发送 ARP 请求 * 如果路由器也“连接”在其中,它会返回一个 ARP Reply `ARP Reply:` ~~~ Sender MAC: target:mac:address:here Sender IP: target.ip.goes.here Target MAC: interface:mac:address:here Target IP: interface.ip.goes.here ~~~ 现在我们有了 DNS 服务器或者默认网关的 IP 地址,我们可以继续 DNS 请求了: * 使用 53 端口向 DNS 服务器发送 UDP 请求包,如果响应包太大,会使用 TCP 协议 * 如果本地/ISP DNS 服务器没有找到结果,它会发送一个递归查询请求,一层一层向高层 DNS 服务器做查询,直到查询到起始授权机构,如果找到会把结果返回 ## 建立连接 ### 使用套接字 当浏览器得到了目标服务器的 IP 地址,以及 URL 中给出来端口号(http 协议默认端口号是 80, https 默认端口号是 443),它会调用系统库函数 socket ,请求一个 TCP流套接字,对应的参数是 AF_INET/AF_INET6 和 SOCK_STREAM 。 * 这个请求首先被交给传输层,在传输层请求被封装成 TCP segment。目标端口会被加入头部,源端口会在系统内核的动态端口范围内选取(Linux下是ip_local_port_range) * TCP segment 被送往网络层,网络层会在其中再加入一个 IP 头部,里面包含了目标服务器的IP地址以及本机的IP地址,把它封装成一个IP packet。 * 这个 TCP packet 接下来会进入链路层,链路层会在封包中加入 frame 头部,里面包含了本地内置网卡的MAC地址以及网关(本地路由器)的 MAC 地址。像前面说的一样,如果内核不知道网关的 MAC 地址,它必须进行 ARP 广播来查询其地址。 到了现在,TCP 封包已经准备好了,可以使用下面的方式进行传输: * [以太网](http://en.wikipedia.org/wiki/IEEE_802.3) * [WiFi](https://en.wikipedia.org/wiki/IEEE_802.11) * [蜂窝数据网络](https://en.wikipedia.org/wiki/Cellular_data_communication_protocol) 对于大部分家庭网络和小型企业网络来说,封包会从本地计算机出发,经过本地网络,再通过调制解调器把数字信号转换成模拟信号,使其适于在电话线路,有线电视光缆和无线电话线路上传输。在传输线路的另一端,是另外一个调制解调器,它把模拟信号转换回数字信号,交由下一个 网络节点 处理。节点的目标地址和源地址将在后面讨论。 大型企业和比较新的住宅通常使用光纤或直接以太网连接,这种情况下信号一直是数字的,会被直接传到下一个 网络节点 进行处理。 最终封包会到达管理本地子网的路由器。在那里出发,它会继续经过自治区域(autonomous system, 缩写 AS)的边界路由器,其他自治区域,最终到达目标服务器。一路上经过的这些路由器会从IP数据报头部里提取出目标地址,并将封包正确地路由到下一个目的地。IP数据报头部 time to live (TTL) 域的值每经过一个路由器就减1,如果封包的TTL变为0,或者路由器由于网络拥堵等原因封包队列满了,那么这个包会被路由器丢弃。 上面的发送和接受过程在 TCP 连接期间会发生很多次: * 客户端选择一个初始序列号(ISN),将设置了 SYN 位的封包发送给服务器端,表明自己要建立连接并设置了初始序列号 * 服务器端接收到 SYN 包,如果它可以建立连接: * 服务器端选择它自己的初始序列号 * 服务器端设置 SYN 位,表明自己选择了一个初始序列号 * 服务器端把 (客户端ISN + 1) 复制到 ACK 域,并且设置 ACK 位,表明自己接收到了客户端的第一个封包 * 客户端通过发送下面一个封包来确认这次连接: * 自己的序列号+1 * 接收端 ACK+1 * 设置 ACK 位 * 数据通过下面的方式传输: * 当一方发送了N个 Bytes 的数据之后,将自己的 SEQ 序列号也增加N * 另一方确认接收到这个数据包(或者一系列数据包)之后,它发送一个 ACK 包,ACK 的值设置为接收到的数据包的最后一个序列号 * 关闭连接时: * 要关闭连接的一方发送一个 FIN 包 * 另一方确认这个 FIN 包,并且发送自己的 FIN 包 * 要关闭的一方使用 ACK 包来确认接收到了 FIN ### TLS 握手 * 客户端发送一个 ClientHello 消息到服务器端,消息中同时包含了它的 Transport Layer Security (TLS) 版本,可用的加密算法和压缩算法。 * 服务器端向客户端返回一个 ServerHello 消息,消息中包含了服务器端的TLS版本,服务器所选择的加密和压缩算法,以及数字证书认证机构(Certificate Authority,缩写 CA)签发的服务器公开证书,证书中包含了公钥。客户端会使用这个公钥加密接下来的握手过程,直到协商生成一个新的对称密钥 * 客户端根据自己的信任CA列表,验证服务器端的证书是否可信。如果认为可信,客户端会生成一串伪随机数,使用服务器的公钥加密它。这串随机数会被用于生成新的对称密钥 * 服务器端使用自己的私钥解密上面提到的随机数,然后使用这串随机数生成自己的对称主密钥 * 客户端发送一个 Finished 消息给服务器端,使用对称密钥加密这次通讯的一个散列值 * 服务器端生成自己的 hash 值,然后解密客户端发送来的信息,检查这两个值是否对应。如果对应,就向客户端发送一个 Finished 消息,也使用协商好的对称密钥加密 * 从现在开始,接下来整个 TLS 会话都使用对称秘钥进行加密,传输应用层(HTTP)内容 ## 发送 HTTP 请求 建立起安全的加密信道后,浏览器开始发送 HTTP 请求,一个请求报文由请求行、请求头、空行、实体(Get 请求没有)组成。 请求头由通用首部、请求首部、实体首部、扩展首部组成。其中,通用首部表示无论是请求报文还是响应报文都可以使用,比如 Date;请求首部表示只有在请求报文中才有意义,分为 Accept 首部、条件请求首部、安全请求首部和代理请求首部这四类;实体首部作用于实体内容,分为内容首部和缓存首部这两类;扩展首部表示用户自定义的首部,通过 X- 前缀来添加。 另外需要注意的是,HTTP 请求头是不区分大小写的,它基于 ASCII 进行编码,而实体可以基于其它编码方式,由 Content-Type 决定。 ~~~ GET / HTTP/1.1 Host: google.com Connection: close [其他头部] ~~~ “其他头部”包含了一系列的由冒号分割开的键值对,它们的格式符合HTTP协议标准,它们之间由一个换行符分割开来。(这里我们假设浏览器没有违反HTTP协议标准的bug,同时假设浏览器使用 HTTP/1.1 协议,不然的话头部可能不包含 Host 字段,同时 GET 请求中的版本号会变成 HTTP/1.0 或者 HTTP/0.9 。) HTTP/1.1 定义了“关闭连接”的选项 "close",发送者使用这个选项指示这次连接在响应结束之后会断开。例如: > Connection:close 不支持持久连接的 HTTP/1.1 应用必须在每条消息中都包含 "close" 选项。 在发送完这些请求和头部之后,浏览器发送一个换行符,表示要发送的内容已经结束了。 ## 返回 HTTP 响应 服务器端返回一个响应码,指示这次请求的状态,响应的形式是这样的: ~~~ 200 OK [响应头部] ~~~ 然后是一个换行,接下来有效载荷(payload),也就是 www.google.com 的HTML内容。服务器下面可能会关闭连接,如果客户端请求保持连接的话,服务器端会保持连接打开,以供之后的请求重用。 如果浏览器发送的HTTP头部包含了足够多的信息(例如包含了 Etag 头部),以至于服务器可以判断出,浏览器缓存的文件版本自从上次获取之后没有再更改过,服务器可能会返回这样的响应: ~~~ 304 Not Modified [响应头部] ~~~ 这个响应没有有效载荷,浏览器会从自己的缓存中取出想要的内容。 在解析完 HTML 之后,浏览器和客户端会重复上面的过程,直到HTML页面引入的所有资源(图片,CSS,favicon.ico等等)全部都获取完毕,区别只是头部的 `GET / HTTP/1.1` 会变成 `GET /$(相对www.google.com的URL) HTTP/1.1 `。 如果HTML引入了 `www.google.com` 域名之外的资源,浏览器会回到上面解析域名那一步,按照下面的步骤往下一步一步执行,请求中的 Host 头部会变成另外的域名。 ## HTTP 服务器请求处理 HTTPD(HTTP Daemon)在服务器端处理请求/响应。最常见的 HTTPD 有 Linux 上常用的 Apache 和 nginx,以及 Windows 上的 IIS。 * HTTPD 接收请求 * 服务器把请求拆分为以下几个参数: * HTTP 请求方法(GET, POST, HEAD, PUT, DELETE, CONNECT, OPTIONS, 或者 TRACE)。直接在地址栏中输入 URL 这种情况下,使用的是 GET 方法 * 域名:google.com * 请求路径/页面:/ (我们没有请求google.com下的指定的页面,因此 / 是默认的路径) * 服务器验证其上已经配置了 google.com 的虚拟主机 * 服务器验证 google.com 接受 GET 方法 * 服务器验证该用户可以使用 GET 方法(根据 IP 地址,身份信息等) * 如果服务器安装了 URL 重写模块(例如 Apache 的 mod_rewrite 和 IIS 的 URL Rewrite),服务器会尝试匹配重写规则,如果匹配上的话,服务器会按照规则重写这个请求 * 服务器根据请求信息获取相应的响应内容,这种情况下由于访问路径是 "/" ,会访问首页文件(你可以重写这个规则,但是这个是最常用的)。 * 服务器会使用指定的处理程序分析处理这个文件,假如 Google 使用 PHP,服务器会使用 PHP 解析 index 文件,并捕获输出,把 PHP 的输出结果返回给请求者 ## 维持连接 完成一次 HTTP 请求后,服务器并不是马上断开与客户端的连接。在 HTTP/1.1 中,`Connection: keep-alive` 是默认启用的,表示持久连接,以便处理不久后到来的新请求,无需重新建立连接而增加慢启动开销,提高网络的吞吐能力。在反向代理软件 Nginx 中,持久连接超时时间默认值为 75 秒,如果 75 秒内没有新到达的请求,则断开与客户端的连接。同时,浏览器每隔 45 秒会向服务器发送 `TCP keep-alive` 探测包,来判断 TCP 连接状况,如果没有收到 ACK 应答,则主动断开与服务器的连接。注意,`HTTP keep-alive` 和 `TCP keep-alive` 虽然都是一种保活机制,但是它们完全不相同,一个作用于应用层,一个作用于传输层。 ## 断开连接 1. 服务器向客户端发送 Alert 报文,类型为 Close Notify,通知客户端不再发送数据,即将关闭连接,同样,这条报文也是经过加密处理的。 2. 服务器通过调用 close 函数主动关闭连接,向客户端发送带有 FIN 标志位的分组,序列号为 m。 3. 客户端确认收到该分组,向服务器发送带有 ACK 标志位的分组,确认号为 m+1。 4. 客户端发送完所有数据后,向服务器发送带有 FIN 标志位的分组,序列号为 n。 5. 服务器确认收到该分组,向客户端发送带有 ACK 标志位的分组,序列号为 n+1。客户端收到确认分组后,立即进入 CLOSED 状态;同时,服务器等待 2 个 MSL(Maximum Segment Lifetime,最大报文生存时间) 的时间后,进入 CLOSED 状态。 ## 浏览器解析过程 现代浏览器是一个及其庞大的大型软件,在某种程度上甚至不亚于一个操作系统,它由多媒体支持、图形显示、GPU 渲染、进程管理、内存管理、沙箱机制、存储系统、网络管理等大大小小数百个组件组成。虽然开发者在开发 Web 应用时,无需关心底层实现细节,只需将页面代码交付于浏览器计算,就可以展示出丰富的内容。但页面性能不仅仅关乎浏览器的实现方式,更取决于开发者的水平,对工具的熟悉程度,代码优化是无止尽的。显然,了解浏览器的基本原理,了解 W3C 技术标准,了解网络协议,对设计、开发一个高性能 Web 应用帮助非常大。 当我们在使用 Chrome 浏览器时,其背后的引擎是 Google 开源的 Chromium 项目,而 Chromium 的内核则是渲染引擎 Blink(基于 Webkit)和 JavaScript 引擎 V8。在阐述浏览器解析 HTML 文件之前,先简单介绍一下 Chromium 的多进程多线程架构(图 5),它包括多个进程: * 一个 Browser 进程 * 多个 Renderer 进程 * 一个 GPU 进程 * 多个 NPAPI Render 进程 * 多个 Pepper Plugin 进程 而每个进程包括若干个线程: 一个主线程 在 Browser 进程中:渲染更新界面 在 Renderer 进程中:使用持有的内核 Blink 实例解析渲染更新界面 一个 IO 线程 在 Browser 进程中:处理 IPC 通信和网络请求 在 Renderer 进程中:处理与 Browser 进程之间的 IPC 通信 一组专用线程 一个通用线程池 ![](https://box.kancloud.cn/82a4510d1954a1ca62537766fd647121_700x649.png) Chromium 支持多种不同的方式管理 Renderer 进程,不仅仅是每一个开启的 Tab 页面,iframe 页面也包括在内,每个 Renderer 进程是一个独立的沙箱,相互之间隔离不受影响。 * Process-per-site-instance:每个域名开启一个进程,并且从一个页面链接打开的新页面共享一个进程(noopener 属性除外),这是默认模式 * Process-per-site:每个域名开启一个进程 * Process-per-tab:每个 Tab 页面开启一个进程 * Single process:所有页面共享一个进程 当 Renderer 进程需要访问网络请求模块(XHR、Fetch),以及访问存储系统(同步 Local Storage、同步 Cookie、异步 Cookie Store)时,则调用 RenderProcess 全局对象通过 IO 线程与 Browser 进程中的 RenderProcessHost 对象建立 IPC 信道,底层通过 socketpair 来实现。正由于这种机制,Chromium 可以更好地统一管理资源、调度资源,有效地减少网络、性能开销。 ### 主流程 页面的解析工作是在 Renderer 进程中进行的,Renderer 进程通过在主线程中持有的 Blink 实例边接收边解析 HTML 内容,每次从网络缓冲区中读取 8KB 以内的数据。浏览器自上而下逐行解析 HTML 内容,经过词法分析、语法分析,构建 DOM 树。 当遇到外部 CSS 链接时,主线程调用网络请求模块异步获取资源,不阻塞而继续构建 DOM 树。当 CSS 下载完毕后,主线程在合适的时机解析 CSS 内容,经过词法分析、语法分析,构建 CSSOM 树。浏览器结合 DOM 树和 CSSOM 树构建 Render 树,并计算布局属性,每个 Node 的几何属性和在坐标系中的位置,最后进行绘制展示在屏幕上。 当遇到外部 JS 链接时,主线程调用网络请求模块异步获取资源,由于 JS 可能会修改 DOM 树和 CSSOM 树而造成回流和重绘,此时 DOM 树的构建是处于阻塞状态的。但主线程并不会挂起,浏览器会使用一个轻量级的扫描器去发现后续需要下载的外部资源,提前发起网络请求,而脚本内部的资源不会识别,比如 document.write。当 JS 下载完毕后,浏览器调用 V8 引擎在 Script Streamer 线程中解析、编译 JS 内容,并在主线程中执行。 :-: ![](https://box.kancloud.cn/f0808ef6881295970e12d29cbb632741_624x289.png) :-: Webkit 主流程 <br> :-: ![](https://box.kancloud.cn/83df18c74eafc1d01077c043a9e05ef4_640x162.png) :-: V8 解释流程,Chrome 66 以前对比 Chrome 66 ### HTML解析 HTML解析是浏览器的HTML解析器把HTML解析成dom tree,而在解析过程,浏览器根据HTML文件的结构从上到下解析html,HTML元素是以深度优先的方式解析,而script、link、style等标签会使解析过程产生阻塞,阻塞的情况有: * 外部样式会阻塞内部脚本的执行 * 外部样式与外部脚本并行加载,但外部样式会阻塞外部脚本执行 * 如果外部脚本带有async属性,则外部脚本的加载与执行不受外部样式影响 * 如果link标签是动态创建(js生成),不管有无async属性,都不会阻塞外部脚本的加载与执行 ### CSS解析 CSS Parser作用就是将很多个CSS文件中的样式合并解析出具有树形结构Style Rules,在对样式解析的过程中,默认CSS选择器是从右往左进行解析的。 ### 脚本执行 浏览器解析HTML时,当遇到`<script>`标签就会立即解析脚本,同时阻塞解析文档直到脚本执行完毕(你可能问为什么要这样设计,明显啊,脚本的执行是改变css和dom,会造成render tree不停的重绘和重排的),而当`<script>`是引入外部js文件时,会阻塞到js文件下载完成并且执行完成为止(除非加了defer或者async属性)。脚本在解析过程中将对dom或css的操作解析出来加入到DOM Tree和cssom中。 ## 渲染流程 当 DOM 树构建完毕后,还需经过好几次转换,它们有多种中间表示。首先计算布局、绘图样式,转换为 RenderObject 树(也叫 Render 树)。再转换为 RenderLayer 树,当 RenderObject 拥有同一个坐标系(比如 canvas、absolute)时,它们会合并为一个 RenderLayer,这一步由 CPU 负责合成。接着转换为 GraphicsLayer 树,当 RenderLayer 满足合成层条件(比如 transform,熟知的硬件加速)时,会有自己的 GraphicsLayer,否则与父节点合并,这一步同样由 CPU 负责合成。最后,每个 GraphicsLayer 都有一个 GraphicsContext 对象,负责将层绘制成位图作为纹理上传给 GPU,由 GPU 负责合成多个纹理,最终显示在屏幕上。 :-: ![](https://box.kancloud.cn/0f6e15daad61666a71d1488c967b2993_650x268.png) :-: 从 DOM 树到 GraphicsLayer 树的转换 另外,为了提升渲染性能效率,浏览器会有专用的 Compositor 线程来负责层合成,同时负责处理部分交互事件(比如滚动、触摸),直接响应 UI 更新而不阻塞主线程。主线程把 RenderLayer 树同步给 Compositor 线程,由它开启多个 Rasterizer 线程,进行光栅化处理,在可视区域以瓦片为单位把顶点数据转换为片元,最后交付给 GPU 进行最终合成渲染。 :-: ![](https://box.kancloud.cn/3ba88393023a38b0e90e903c40bdf822_720x331.png) :-: Chromium 多线程渲染 ## 页面生命周期 页面从发起请求开始,结束于跳转、刷新或关闭,会经过多次状态变化和事件通知,因此了解整个过程的生命周期非常有必要。浏览器提供了 [Navigation Timing](https://link.zhihu.com/?target=https%3A//www.w3.org/TR/navigation-timing-2/) 和 [Resource Timing](https://link.zhihu.com/?target=https%3A//www.w3.org/TR/resource-timing-2/) 两种 API 来记录每一个资源的事件发生时间点,你可以用它来收集 RUM(Real User Monitoring,真实用户监控)数据,发送给后端监控服务,综合分析页面性能来不断改善用户体验。下图表示 HTML 资源加载的事件记录全过程,而中间黄色部分表示其它资源(CSS、JS、IMG、XHR)加载事件记录过程,它们都可以通过调用 `window.performance.getEntries()` 来获取具体指标数据。 :-: ![](https://box.kancloud.cn/ba4c5ac012a0e60ac57858da133d82ee_720x226.png) :-: 页面加载事件记录流程 衡量一个页面性能的方式有很多,但能给用户带来直接感受的是页面何时渲染完成、何时可交互、何时加载完成。其中,有两个非常重要的生命周期事件,`DOMContentLoaded` 事件表示 DOM 树构建完毕,可以安全地访问 DOM 树所有 Node 节点、绑定事件等等;load 事件表示所有资源都加载完毕,图片、背景、内容都已经完成渲染,页面处于可交互状态。但是迄今为止浏览器并不能像 Android 和 iOS app 一样完全掌控应用的状态,在前后台切换的时候,重新分配资源,合理地利用内存。实际上,现代浏览器都已经在做这方面的相关优化,并且自 Chrome 68 以后提供了[`Page Lifecycle`](https://link.zhihu.com/?target=https%3A//wicg.github.io/page-lifecycle/spec.html) API,定义了全新的浏览器生命周期,让开发者可以构建更出色的应用。 :-: ![](https://box.kancloud.cn/284d43e333924fc081f16328692d4ecb_720x487.png) :-: 新版页面生命周期 现在,你可以通过给 window 和 document 绑定上所有生命周期监听事件,来监测页面切换、用户交互行为所触发的状态变化过程。不过,开发者只能感知事件在何时发生,不能直接获取某一刻的页面状态(图中的 STATE)。即使如此,利用这个 API,也可以让脚本在合适的时机执行某项任务或进行界面 UI 反馈。 ![](https://box.kancloud.cn/dfb02dd3018e45a337cce2e6076f33d0_720x349.png) # 参考资料 [当···时发生了什么?](https://github.com/skyline75489/what-happens-when-zh_CN#gpu) [浏览器输入 URL 后发生了什么?](https://zhuanlan.zhihu.com/p/43369093) [一篇文章搞定前端面试](https://juejin.im/post/5bbaa549e51d450e827b6b13#heading-5)