输入URL的时候
浏览器根据自己的算法,以及你是否处于隐私浏览模式,会在浏览器的地址框下方给出输入建议。大部分算法会优先考虑根据你的搜索历史和书签等内容给出建议。
同时,浏览器预判断是否是历史请求的url,并计算请求的概率,可能为了加快请求速度而预先在后台与几项历史相似记录建立tcp连接。
敲入回车(或触摸屏确认)
若敲入回车, 在这个时刻,一个专用于回车键的电流回路被直接地或者通过电容器间接地闭合了,使得少量的电流进入了键盘的逻辑电路系统。这个系统会扫描每个键的状态,对于按键开关的电位弹跳变化进行噪音消除,并将其转化为键盘码值。在这里,回车的码值是13。键盘控制器在得到码值之后,将其编码,将信号传给cpu。【原理是利用金属把两个触点接通或断开以输入信号,或者利用霍尔效应开关(利用磁场变化)和电容开关(利用电流和电压变化)产生输入信号】(注: 当一块通有电流的金属或半导体薄片垂直地放在磁场中时,薄片的两端就会产生电位差,这种现象就称为霍尔效应。)
若触摸屏确认, 在现代电容屏上,当用户把手指放在屏幕上时,一小部分电流从传导层的静电域经过手指传导,形成了一个回路,使得屏幕上触控的那一点电压下降,屏幕控制器产生一个中断,报告这次“点击”的坐标,然后移动操作系统通知当前活跃的应用,有一个点击事件发生在它的某个GUI部件上了,现在这个部件是虚拟键盘的按钮,虚拟键盘引发一个软中断,返回给OS一个“按键按下”消息, 这个消息又返回来向当前活跃的应用通知一个“按键按下”事件。
从内存到CPU
通过电压高低的变化接收到信号,电流流经晶体管的半导体、这些晶体管构成集成电路, 通过电压就能控制线路开闭, 实现「与」「或」「非」等逻辑电路门,对信息计算处理,同时由寄存器存储单元来加载和存储数据。简单点说, 数据从输入设备流经内存,等待CPU的处理,这些将要处理的信息是按字节存储的,也就是以8位二进制数或8比特为1个单元存储。(这些信息可以是数据或指令。数据可以是二进制表示的字符、数字或颜色等等。而指令告诉CPU对数据执行哪些操作,比如完成加法、减法或移位运算)
从 CPU 到操作系统内核
CPU接收到信号后会触发 CPU 的中断机制(中断Interrupt是指处理器接收到来自硬体或软体的信号,提示发生了某些事件,应该被注意,这种情况就称为中断),当中断发生时,CPU 会停下当前运行的程序,保存当前执行状态(如 PC 值( PC是一个16位的计数器。用于存放和指示下一条要执行的指令的地址)),进入 IRQ 状态(中断处理线状态),然后跳转到对应的中断处理程序执行,这个程序由内核驱动来实现。
从操作系统到浏览器
由操作系统内核驱动完成对硬件信号的抽象(Mac OS X把信号翻译成键码值,Windows 把键盘按下的事件传送给驱动,把HID的信号转换成一个扫描码),然后将事件分发给合适的(活跃的,或者正在监听的)应用程序(如浏览器或APP)。
浏览器解析URL
浏览器解析URL获取协议,主机,端口,路径。解析过程没有协议的默认加上HTTP协议,没有端口的默认为对应协议默认端口,(如HTTP的80端口,HTTPS的443端口,FTP的21端口,SSH的22端口等等),并且对URL检查输入是否含有不是 a-z, A-Z,0-9, - 或者 . 的字符,有的话进行unicode编码。检查主机是域名还是IP地址,若是IP,跳过DNS查询(域名解析)。
检查 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 服务器做查询,直到查询到起始授权机构,如果找到会把结果返回。
即会先从 root nameservers 找起。 即是假如你要查询 www.example.com ,会先从包含根结点的 13 台最高级域名服务器开始。
接着,以从右向左的方式递进,找到 com. 然后向包含 com 的 TLD(顶级域名) nameservers 发送 DNS 请求。接着找到包含 example 的 DNS server。
现在进入到了example.com 部分,即是现在正在询问的是权威服务器,该服务器里面包含了你想要的域名信息,也就是拿到了最后的结果 record 。
递归查询的 DNS Server 接受到这 record 之后, 会将该record 保存一份到本地。 如果下一次你再请求这个 domain 时,我就可以直接返回给你了。由于每条记录都会存在 TLL ,所以 server 每隔一段时间都会发送一次请求,获取新的 record,
最后,再经由最近的 DNS Server 将该条 record 返回。 同样,你的设备也会存一份该 record 的副本。 之后,就是 TCP 的事了。
注:DNS 解析其实是一个很复杂的过程,在 PC 上,我们采用域名发散策略( 目的是充分利用现代浏览器的多线程并发下载能力。由于浏览器的限制,每个浏览器,允许对每个域名的连接数一般是有上限的),是因为在 PC 端上,DNS 解析通常而言只需要几十 ms ,可以接受。而移动端,2G 网络,3G网络,4G网络/wifi 强网,而且移动 4G 容易在信号不理想的地段降级成 2G ,通过大量的数据采集和真实网络抓包分析(存在DNS解析的请求),DNS的消耗相当可观,2G网络大量5-10s,3G网络平均也要3-5s(数据来源于淘宝)。
因为在增加域的同时,往往会给浏览器带来 DNS 解析的开销。所以在这种情况下,提出了域名收敛,减少域名数量可以降低 DNS 解析的成本。
建立tcp连接
好,经过dns查询我们已经得到了目标ip,加上端口号, 调用系统库函数 socket,打开一个socket与目标IP地址的该端口建立TCP链接。
TCP 封包
这个请求首先被交给传输层,在传输层请求被封装成 TCP segment。目标端口会被加入头部,源端口会在系统内核的动态端口范围内选取(Linux下是ip_local_port_range)
TCP segment 被送往网络层,网络层会在其中再加入一个 IP 头部,里面包含了目标服务器的IP地址以及本机的IP地址,把它封装成一个TCP packet。
这个 TCP packet 接下来会进入链路层,链路层会在封包中加入 frame头 部,里面包含了本地内置网卡的MAC地址以及网关(本地路由器)的 MAC 地址。像前面说的一样,如果内核不知道网关的 MAC 地址,它必须进行 ARP 广播来查询其地址。
传输
有几种传输方式
1、以太网
2、WiFi
3、蜂窝数据网络
对于大部分家庭网络和小型企业网络来说,封包会从本地计算机出发,经过本地网络,再通过调制解调器把数字信号转换成模拟信号,使其适于在电话线路,有线电视光缆和无线电话线路上传输。在传输线路的另一端,是另外一个调制解调器,它把模拟信号转换回数字信号,交由下一个 网络节点 处理。节点的目标地址和源地址将在后面讨论。
大型企业和比较新的住宅通常使用光纤或直接以太网连接,这种情况下信号一直是数字的,会被直接传到下一个 网络节点 进行处理。
最终封包会到达管理本地子网的路由器。在那里出发,它会继续经过自治区域(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
注:通俗点理解tcp:
三次握手(为什么是三次,主要是如果是两次握手建立连接,由于网络堵塞或其它原因导致请求端请求来迟,而接收端开辟等待信息接口后没有后续信息到来(可能已失效),影响性能和内存损耗,所以要三次握手来确认双方都是在活跃状态的)
第一次:喂,听得到吗
第二次:听得到 ,确定要连接吗
第三次:确定,巴拉巴拉..
四次挥手
第一次:我不给你数据了啊
第二次:好的收到,还有一些数据没发完,完了我告诉你
第三次:数据发完了,我匿了
第四次:真的吗,等几秒没回复,好的我也匿了
注:为什么HTTP协议要基于TCP来实现?TCP是一个端到端的可靠的面向连接的协议,所以HTTP基于传输层TCP协议不用担心数据的传输的各种问题。
TLS 握手
客户端发送一个 ClientHello 消息到服务器端,消息中同时包含了它的 Transport Layer Security (TLS) 版本,可用的加密算法和压缩算法。
服务器端向客户端返回一个 ServerHello 消息,消息中包含了服务器端的TLS版本,服务器所选择的加密和压缩算法,以及数字证书认证机构(Certificate Authority,缩写 CA)签发的服务器公开证书,证书中包含了公钥。客户端会使用这个公钥加密接下来的握手过程,直到协商生成一个新的对称密钥
客户端根据自己的信任CA列表,验证服务器端的证书是否可信。如果认为可信,客户端会生成一串伪随机数,使用服务器的公钥加密它。这串随机数会被用于生成新的对称密钥
服务器端使用自己的私钥解密上面提到的随机数,然后使用这串随机数生成自己的对称主密钥
客户端发送一个 Finished 消息给服务器端,使用对称密钥加密这次通讯的一个散列值
服务器端生成自己的 hash 值,然后解密客户端发送来的信息,检查这两个值是否对应。如果对应,就向客户端发送一个Finished 消息,也使用协商好的对称密钥加密
从现在开始,接下来整个 TLS 会话都使用对称秘钥进行加密,传输应用层(HTTP)内容
浏览器查看缓存
url解析后, 浏览器会查看缓存,
如果资源未缓存,发起新请求
如果请求资源在缓存中, 不同的浏览器,不同的用户操作,不同缓存策略,会有对缓存不同的处理(详见这里)。总的来说,要么直接在缓存中取数据,要么发起请求检验缓存是否有效,返回304就是未修改,返回200就需要对缓存更新。
缓存策略通常有几种方式
- HTTP1.0提供Expires, Expires 头部字段提供一个日期和时间,响应在该日期和时间后被认为失效。失效的缓存条目通常不会被缓存。格式一般为 "Expires: Sun, 08 Nov 2009 03:37:26 GMT"
- HTTP1.1增加了Cache-Control: max-age=,值为以秒为单位, 而如果指定了max-age值,那么在此值内的时间里就不会重新访问服务器,例如:Cache-control: max-age=5 表示当访问此网页后的5秒内再次访问不会去服务器. (注意:cache-control max-age 和 s-maxage 将覆盖 Expires 头部。)
- Last-Modified
在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是你请求的资源,同时有一个Last-Modified的属性标记(HttpReponse Header)此文件在服务期端最后被修改的时间,格式类似这样:
Last-Modified:Tue, 24 Feb 2009 08:01:04 GMT
客户端第二次请求此URL时,根据HTTP协议的规定,浏览器会向服务器传送If-Modified-Since报头(HttpRequest Header),询问该时间之后文件是否有被修改过:
If-Modified-Since:Tue, 24 Feb 2009 08:01:04 GMT - ETag
ETag 响应头部字段值是一个实体标记,它提供一个 “不透明” 的缓存验证器。这可能在以下几种情况下提供更可靠的验证:不方便存储修改日期;HTTP 日期值的 one-second 解决方案不够用;或者原始服务器希望避免由于使用修改日期而导致的某些冲突。
在典型用法中,当一个URL被请求,Web服务器会返回资源和其相应的ETag值,它会被放置在HTTP的“ETag”字段中:
ETag: "686897696a7c876b7e"
然后,客户端可以决定是否缓存这个资源和它的ETag。以后,如果客户端想再次请求相同的URL,将会发送一个包含已保存的ETag和“If-None-Match”字段的请求。
If-None-Match: "686897696a7c876b7e"
客户端请求之后,服务器可能会比较客户端的ETag和当前版本资源的ETag。如果ETag值匹配,这就意味着资源没有改变,服务器便会发送回一个极短的响应,包含HTTP “304 未修改”的状态。304状态告诉客户端,它的缓存版本是最新的,并应该使用它。
然而,如果ETag的值不匹配,这就意味着资源很可能发生了变化,那么,一个完整的响应就会被返回,包括资源的内容,就好像ETag没有被使用。这种情况下,客户端可以用新返回的资源和新的ETag替代先前的缓存版本。
HTTP请求
浏览器组装http get 请求报文,通过tcp通道发送给目标服务器。
HTTP头字段列表
负载均衡
请求在进入到真正的应用服务器前,可能还会先经过负责负载均衡的机器,它的作用是将请求合理地分配到多个服务器上,同时具备具备防攻击等功能。
负载均衡具体实现有很多种,有直接基于硬件的 F5,有操作系统传输层(TCP)上的 LVS,也有在应用层(HTTP)实现的反向代理(也叫七层代理),接下来将介绍 LVS 及反向代理。
负载均衡的策略也有很多,如果后面的多个服务器性能均衡,最简单的方法就是挨个循环一遍(Round-Robin),其它策略就不一一介绍了,可以参考 LVS 中的算法。
- LVS
LVS 的作用是从对外看来只有一个 IP,而实际上这个 IP 后面对应是多台机器,因此也被成为 Virtual IP。
前面提到的 NAT 也是一种 LVS 中的工作模式,除此之外还有 DR 和 TUNNEL,具体细节这里就不展开了,它们的缺点是无法跨网段,所以百度自己开发了 BVS 系统。
- 反向代理
反向代理是工作在 HTTP 上的,具体实现可以基于 HAProxy 或 Nginx,因为反向代理能理解 HTTP 协议,所以能做非常多的事情,比如:
进行很多统一处理,比如防攻击策略、防抓取、SSL、gzip、自动性能优化等
应用层的分流策略都能在这里做,比如对 /xx 路径的请求分到 a 服务器,对 /yy 路径的请求分到 b 服务器,或者按照 cookie 进行小流量测试等
缓存,并在后端服务挂掉的时候显示友好的 404 页面
监控后端服务是否异常
HTTP 服务器请求处理
数据已经到达服务器网卡之后,接着网卡会将数据拷贝到内存中(DMA 直接内存存取 它允许不同速度的硬件装置来沟通,而不需要依赖于CPU 的大量中断负载。 否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。),然后通过中断来通知 CPU。cpu通过时钟机制对指令进行处理。同样经由操作系统内核驱动对硬件信息进行抽象,把信息传递给对应的处理程序。服务端的的服务器(apache,nginx,iis, Tomcat,nodejs等)通过监听端口,来接收并处理请求信息。
服务器把请求拆分为以下几个参数:
- HTTP 请求方法(GET, POST, HEAD, PUT, DELETE, CONNECT, OPTIONS, 或者 TRACE)。直接在地址栏中输入 URL 这种情况下,使用的是 GET 方法
- 域名 如example.com
- 请求路径/页面
服务器验证 example .com 接受 GET 方法
服务器验证该用户可以使用 GET 方法(根据 IP 地址,身份信息等)
如果服务器安装了 URL 重写模块(例如 Apache 的 mod_rewrite 和 IIS 的 URL Rewrite),服务器会尝试匹配重写规则,如果匹配上的话,服务器会按照规则重写这个请求
服务器根据请求信息获取相应的响应内容,这种情况下由于访问路径是 "/" ,会访问首页文件(你可以重写这个规则,但是这个是最常用的)。
服务器会使用指定的处理程序分析处理这个文件,假如使用 PHP,服务器会使用 php-fpm(请求处理进程池) 解析 index 文件,并捕获输出,把 PHP 的输出结果返回给请求者
服务端程序处理
根据路径和查询参数进行相应处理,比如服务端处理程序选用laravel,那么抵达 public/index.php 入口文件之后,laravel会定义项目路径,加载框架核心文件,注册自动加载,错误和异常处理,日志记录机制,初始化应用,解析url,跳转路由,权限检测,分发请求,处理请求(数据库查询,数据过滤,请求其它服务器数据等操作 ),然后返回给浏览器对应的数据包(包括http协议组成的代码。里面包含页面的布局、文字。数据也可能是图片、脚本程序,反应头,反应数据,请求头等),如果开启了gzip之类的功能服务器会先把响应内容通过特定编码压缩后在返回给浏览器。
返回结果到浏览器
浏览器根据响应状态码判断请求的状态。
如常见的:
200(OK)
请求已成功,请求所希望的响应头或数据体将随此响应返回。301(Moved Permanently)
被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个URI之一。如果该请求不是GET/HEAD, 浏览器通常会要求用户确认重定向。
301通常用于网站迁移时,服务器对旧的URL进行301重定向到新的URL。这样搜索引擎可以正确地更新原有的页面排名等信息。302(Found)
请求的资源现在临时从不同的URI响应请求。除非指定了Cache-Control或Expires,否则该响应不可缓存。 如果当前请求非HEAD或GET,浏览器需取得用户确认,再进行重定向。304(Not Modified)
如果客户端发送了一个带条件的GET请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变。 304响应禁止包含消息体。307(Temporary Redirect)
400(Bad Request)
由于包含语法错误,当前请求无法被服务器理解。400通常在服务器端表单验证失败时返回。403(Forbidden)
服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助。404(Not Found)
500(Internal Server Error)
通常是代码出错,后台Bug。一般的Web服务器通常会给出抛出异常的调用堆栈。 然而多数服务器即使在生产环境也会打出调用堆栈,这显然是不安全的。502(Bad Gateway)
作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。
如果你在用HTTP代理来翻墙,或者你配置了nginx来反向代理你的应用,你可能会常常看到它。504(Gateway Time-out)
作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器收到响应。
注意与502的区别:502是接收到了无效响应比如Connection Refused; 504是响应超时,通常是被墙了。
然后如果缓存验证有效,就是使用缓存,缓存失效得到新的数据后进行解析。如果资源可缓存,进行缓存。
浏览器解析数据
如果有 gzip 会先解压,然后接下来最重要的问题是要知道它的编码是什么,比如同样一个「中」字,在 UTF-8 编码下它的内容其实是「11100100 10111000 10101101」也就是「E4 B8 AD」,而在 GBK 下则是「11010110 11010000」,也就是「D6 D0」,如何才能知道文件的编码?可以有很多判断方法:
- 用户设置,在浏览器中可以指定页面编码
- HTTP 协议中
- <meta> 中的 charset 属性值
- 对于 JS 和 CSS
- 对于 iframe
如果在这些地方都没指明,浏览器就很难处理,在它看来就是一堆「0」和「1」,比如「中文」,它在 UTF-8 下有 6 个字节,如果按照 GBK 可以当成「涓枃」这 3 个汉字来解释,浏览器怎么知道到底是「中文」还是「涓枃」呢?
不过正常人一眼就能认出「涓枃」是错的,因为这 3 个字太不常见了,所以有人就想到通过判断常见字的方法来检测编码,典型的比如 Mozilla 的 UniversalCharsetDetection,不过这东东误判率也很高,所以还是指明编码的好。
这样后续对文本的操作就是基于「字符」(Character)的了,一个汉字就是一个字符,不用再关心它究竟是 2 个字节还是 3 个字节。
浏览器对加载到的资源(HTML、JS、CSS等)进行语法解析,建立相应的内部数据结构(如HTML的DOM)
载入解析到的资源文件,渲染页面,完成。
浏览器的渲染过程
解析HTML文档,构件DOM树,下载资源,构造CSSOM树,执行js脚本,这些操作没有严格的先后顺序,以下分别解释
-
构建DOM树: DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
- Tokenizing:根据HTML规范将字符流解析为标记
- Lexing:词法分析将标记转换为对象并定义属性和规则
- DOM construction:根据HTML标记关系将对象组成DOM树
解析过程中遇到图片、样式表、js文件,启动下载
-
构建CSSOM树:
- Tokenizing:字符流转换为标记流
- Node:根据标记创建节点
- CSSOM:节点创建CSSOM树
-
- 有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。下一步操作就是计算出每个节点在屏幕中的位置。从DOM树的根节点遍历所有可见节点,不可见节点包括:1)script,meta这样本身不可见的标签。2)被css隐藏的节点,如display: none
- 对每一个可见节点,找到恰当的CSSOM规则并应用
- 发布可视节点的内容和计算样式
-
js解析如下:
- 浏览器创建Document对象并解析HTML,将解析到的元素和文本节点添加到文档中,此时document.readystate为loading
- HTML解析器遇到没有async和defer的script时,将他们添加到文档中,然后执行行内或外部脚本。这些脚本会同步执行,并且在脚本下载和执行时解析器会暂停。这样就可以用document.write()把文本插入到输入流中。同步脚本经常简单定义函数和注册事件处理程序,他们可以遍历和操作script和他们之前的文档内容
- 当解析器遇到设置了async属性的script时,开始下载脚本并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器不会停下来等它下载。异步脚本禁止使用document.write(),它们可以访问自己script和之前的文档元素
- 当文档完成解析,document.readState变成interactive
- 所有defer脚本会按照在文档出现的顺序执行,延迟脚本能访问完整文档树,禁止使用document.write()
- 浏览器在Document对象上触发DOMContentLoaded事件
- 此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成载入和执行,document.readState变为complete,window触发load事件
显示页面(HTML解析过程中会逐步显示页面)
注:
Repaint——屏幕的一部分要重画,比如某个CSS的背景色变了。但是元素的几何尺寸没有变。
Reflow——意味着元件的几何尺寸变了,我们需要重新验证并计算Render Tree。是Render Tree的一部分或全部发生了变化。这就是Reflow,或是Layout。(HTML使用的是flow based layout,也就是流式布局,所以,如果某元件的几何尺寸发生了变化,需要重新布局,也就叫reflow)reflow 会从<html>这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置,在reflow过程中,可能会增加一些frame,比如一个文本字符串必需被包装起来。
Reflow的成本比Repaint的成本高得多的多。DOM Tree里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow。
参考链接
从输入 URL 到页面加载完成的过程中都发生了什么事情?
浏览器缓存机制
在浏览器地址栏输入一个URL后回车,背后会进行哪些技术步骤?
一次完整的浏览器请求流程
当···时发生了什么?
What happens when...
深度解析URL到页面渲染的全过程
浏览器缓存详解:expires,cache-control,last-modified,etag详细说明
浏览器的工作原理:新式网络浏览器幕后揭秘
浏览器的渲染原理简介
等。