导言
上一篇我们整理了从浏览器接收到url到开启网络请求线程,有兴趣的同学可以看看整理自己的笔记,构建知识体系(前端篇 一),闲话不多说,我们继续。
2 开启网络请求线程发出完整的http请求
DNS域名解析
DNS(Domain Name System):域名解析服务器
DNS根域
根域:目前有13个根集群服务器,美国10台,日本1台,荷兰1台,瑞典1台
一级域名:Top Level Domain: tld com, edu, mil, gov, net, org, int,arpa 组织域、国家域(.cn, .ca, .hk, .tw)、反向域 等
二级域名
三级域名
最多127级域名
如图:
ICANN(The Internet Corporation for Assigned Names and Numbers)互联网名称与数字地址分配机构,负责在全球范围内对互联网通用顶级域名(gTLD)以及国家和地区顶级域名(ccTLD)系统的管理、以及根服务器系统的管理
DNS工作原理
客户端向离它最近的DNS服务器发起了查询请求,一般是由运营商提供 如果代理DNS服务器有记录则直接可以返回给客户端;如果没有记录则去根DNS服务器请求,根DNS并不会存储所以的主机名对应IP的记录,它只会记录它的子域的IP,例如.com等后缀的域,代理DNS服务器会拿到.com域的DNS服务器IP 然后再将请求发往.com.域的DNS服务器,如果还是没有找到主机,则再往它的下一级找,直到找到具体的主机,把IP返回给客户端,同时代理DNS服务器也会缓存一份到本地 一次完整的查询请求经过的流程:Client -->hosts文件 -->DNS Service Local Cache --> DNS Server (recursion) --> Server Cache --> iteration(迭代) --> 根--> 顶级域名DNS-->二级域名DNS…
TCP/IP请求
http的本质就是tcp/ip请求
这里我们需要了解3次握手规则建立连接以及断开连接时的四次挥手
tcp将http长报文划分为短报文,通过三次握手与服务端建立连接,进行可靠传输
三次握手的步骤:(抽象派)
客户端:hello,你是server么?
服务端:hello,我是server,你是client么?
客户端:yes,我是client
建立连接成功后,接下来就正式传输数据
四次挥手的步骤:(抽象派)
主动方:我已经关闭了向你那边的主动通道了,只能被动接收了
被动方:收到通道关闭的信息
被动方:那我也告诉你,我这边向你的主动通道也关闭了
主动方:最后收到数据,之后双方无法通信
为什么连接的时候是三次握手,关闭的时候却是四次握手?
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
五层因特网协议栈
协议
在计算机网络与信息通讯领域里,人们经常提及 “协议” 一词。互联网中常用的协议有HTTP、TCP、IP等。
协议的必要性
简单来说,协议就是计算机与计算机之间通过网络通信时,事先达成的一种 “约定”。这种“约定”使不同厂商的设备、不同的CPU以及不同操作系统组成的计算机之间,只要遵循相同的协议就能够实现通信。这就好比一个中国人说汉语一个外国人说英语使用不同的国家语言进行沟通,怎么也无法理解。如果两个人约定好 都说中文或英文,就可以互相沟通通信。协议分为很多种,每一种协议都明确界定了它的行为规范。两台计算机必须能够支持相同的协议,并遵循相同协议进行处理,这样才能实现相互通信。
协议分层
网络协议通常分不同层次进行开发,每一层分别负责不同的通信功能
分层的作用
这两部分我推荐一篇个人觉得写得比较详细而且易懂的文章 TCP/IP详解3 服务器接收到请求
服务端在接收到请求时,内部会进行很多的处理,但是自己也只做过一点nodejs后端,也没有大型高并发项目经验,所以只是简单介绍一下,留个概念
负载均衡
对于大型的项目,由于并发访问量很大,所以往往一台服务器是吃不消的,所以一般会有若干台服务器组成一个集群,然后配合反向代理实现负载均衡
当然了,负载均衡不止这一种实现方式,这里不深入...
简单的说:
用户发起的请求都指向调度服务器(反向代理服务器,譬如安装了nginx控制负载均衡),然后调度服务器根据实际的调度算法,分配不同的请求给对应集群中的服务器执行,然后调度器等待实际服务器的HTTP响应,并将它反馈给用户
想全面一点了解的同学请看这里
后台处理
一般后台都是部署到容器中的,所以一般为:
先是容器接受到请求(如tomcat容器) 然后对应容器中的后台程序接收到请求(如java程序) 然后就是后台会有自己的统一处理,处理完后响应响应结果 概括下:
一般有的后端是有统一的验证的,如安全拦截,跨域验证 如果这一步不符合规则,就直接返回了相应的http报文(如拒绝请求等) 然后当验证通过后,才会进入实际的后台代码,此时是程序接收到请求,然后执行(譬如查询数据库,大量计算等等) 等程序执行完毕后,就会返回一个http响应包(一般这一步也会经过多层封装) 然后就是将这个包从后端发送到前端,完成交互
4 前后台http交互
http报文
用于HTTP协议交互的信息被称为报文。
请求端(客户端)的HTTP报文叫做请求报文,响应端(服务器端)的叫做响应报文。
HTTP报文本身是由多行数据构成的字符串文本。
HTTP报文大致上可分为报文首部和报文主体两块,两者由最初出现的空行来划分。
通常,并不一定要有报文主体,报文一般包括了:通用头部,请求/响应头部,请求/响应体。
通用头部
Request Url: 请求的web服务器地址
Request Method: 请求方式 (Get、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE)
Status Code: 请求的返回状态码,如200代表成功
Remote Address: 请求的远程服务器地址(会转为IP)
譬如,在跨域拒绝时,可能是method为options,状态码为404/405等(当然,实际上可能的组合有很多)
其中,Method的话一般分为两批次:
HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。
HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。 这里面最常用到的就是状态码,很多时候都是通过状态码来判断 状态码大致范围意思:
请求/响应头
常用请求头:
Accept: 接收类型,表示浏览器支持的MIME类型
(对标服务端返回的Content-Type)
Accept-Encoding:浏览器支持的压缩类型,如gzip等,超出类型不能接收
Content-Type:客户端发送出去实体内容的类型
Cache-Control: 指定请求和响应遵循的缓存机制,如no-cache
If-Modified-Since:对应服务端的Last-Modified,用来匹配看文件是否变动,只能精确到1s之内,http1.0中
Expires:缓存控制,在这个时间内不会请求,直接使用缓存,http1.0,而且是服务端时间
Max-age:代表资源在本地缓存多少秒,有效时间内不会请求,而是使用缓存,http1.1中
If-None-Match:对应服务端的ETag,用来匹配文件内容是否改变(非常精确),http1.1中
Cookie: 有cookie并且同域访问时会自动带上
Connection: 当浏览器与服务器通信时对于长连接如何进行处理,如keep-alive
Host:请求的服务器URL
Origin:最初的请求是从哪里发起的(只会精确到端口),Origin比Referer更尊重隐私
Referer:该页面的来源URL(适用于所有类型的请求,会精确到详细页面地址,csrf拦截常用到这个字段)
User-Agent:用户客户端的一些必要信息,如UA头部等
复制代码
常用响应头:
Access-Control-Allow-Headers: 服务器端允许的请求Headers
Access-Control-Allow-Methods: 服务器端允许的请求方法
Access-Control-Allow-Origin: 服务器端允许的请求Origin头部(譬如为*)
Content-Type:服务端返回的实体内容的类型
Date:数据从服务器发送的时间
Cache-Control:告诉浏览器或其他客户,什么环境可以安全的缓存文档
Last-Modified:请求资源的最后修改时间
Expires:应该在什么时候认为文档已经过期,从而不再缓存它
Max-age:客户端的本地资源应该缓存多少秒,开启了Cache-Control后有效
ETag:请求变量的实体标签的当前值
Set-Cookie:设置和页面关联的cookie,服务器通过这个头部把cookie传给客户端
Keep-Alive:如果客户端有keep-alive,服务端也会有响应(如timeout=38)
Server:服务器的一些相关信息
复制代码
http缓存
前后端的http交互中,使用缓存能很大程度上的提升效率,而且基本上对性能有要求的前端项目都是必用缓存的
强缓存与弱缓存
缓存可以简单的划分成两种类型:强缓存(200 from cache)与协商缓存(304)(这里推荐大佬的好文前端进阶必备的网络基础)
区别简述如下:
强缓存(200 from cache)时,浏览器如果判断本地缓存未过期,就直接使用,无需发起http请求 协商缓存(304)时,浏览器会向服务端发起http请求,然后服务端告诉浏览器文件未改变,让浏览器使用本地缓存 对于协商缓存,使用Ctrl + F5强制刷新可以使得缓存无效
但是对于强缓存,在未过期时,必须更新资源路径才能发起新的请求(更改了路径相当于是另一个资源了,这也是前端工程化中常用到的技巧)
缓存头部简述 上述提到了强缓存和协商缓存,那它们是怎么区分的呢?
答案是通过不同的http头部控制
先看下这几个头部:
If-None-Match/E-tag、If-Modified-Since/Last-Modified、Cache-Control/Max-Age、Pragma/Expires 这些就是缓存中常用到的头部,这里不展开。仅列举下大致使用。
属于强缓存控制的:
(http1.1)Cache-Control/Max-Age (http1.0)Pragma/Expires 注意:Max-Age不是一个头部,它是Cache-Control头部的值
属于协商缓存控制的:
(http1.1)If-None-Match/E-tag (http1.0)If-Modified-Since/Last-Modified 可以看到,上述有提到http1.1和http1.0,这些不同的头部是属于不同http时期的
再提一点,其实HTML页面中也有一个meta标签可以控制缓存方案-Pragma
不过,这种方案还是比较少用到,因为支持情况不佳,譬如缓存代理服务器肯定不支持,所以不推荐头部的区别 首先明确,http的发展是从http1.0到http1.1
而在http1.1中,出了一些新内容,弥补了http1.0的不足。
http1.0中的缓存控制:
Pragma:严格来说,它不属于专门的缓存控制头部,但是它设置no-cache时可以让本地强缓存失效(属于编译控制,来实现特定的指令,主要是因为兼容http1.0,所以以前又被大量应用)
Expires:服务端配置的,属于强缓存,用来控制在规定的时间之前,浏览器不会发出请求,而是直接使用本地缓存,注意,Expires一般对应服务器端时间,如Expires:Fri, 30 Oct 1998 14:19:41 If-Modified-Since/Last-Modified:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是If-Modified-Since,而服务端的是Last-Modified,它的作用是,在发起请求时,如果If-Modified-Since和Last-Modified匹配,那么代表服务器资源并未改变,因此服务端不会返回资源实体,而是只返回头部,通知浏览器可以使用本地缓存。Last-Modified,顾名思义,指的是文件最后的修改时间,而且只能精确到1s以内
http1.1中的缓存控制:
Cache-Control:缓存控制头部,有no-cache、max-age等多种取值
Max-Age:服务端配置的,用来控制强缓存,在规定的时间之内,浏览器无需发出请求,直接使用本地缓存,注意,Max-Age是Cache-Control头部的值,不是独立的头部,譬如Cache-Control: max-age=3600,而且它值得是绝对时间,由浏览器自己计算
If-None-Match/E-tag:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是If-None-Match,而服务端的是E-tag,同样,发出请求后,如果If-None-Match和E-tag匹配,则代表内容未变,通知浏览器使用本地缓存,和Last-Modified不同,E-tag更精确,它是类似于指纹一样的东西,基于FileEtag INode Mtime Size生成,也就是说,只要文件变,指纹就会变,而且没有1s精确度的限制。
Max-Age相比Expires?
Expires使用的是服务器端的时间
但是有时候会有这样一种情况-客户端时间和服务端不同步
那这样,可能就会出问题了,造成了浏览器本地的缓存无用或者一直无法过期
所以一般http1.1后不推荐使用Expires
而Max-Age使用的是客户端本地时间的计算,因此不会有这个问题
因此推荐使用Max-Age。
注意,如果同时启用了Cache-Control与Expires,Cache-Control优先级高。
E-tag相比Last-Modified?
Last-Modified:
表明服务端的文件最后何时改变的 它有一个缺陷就是只能精确到1s, 然后还有一个问题就是有的服务端的文件会周期性的改变,导致缓存失效 而E-tag:
是一种指纹机制,代表文件相关指纹 只有文件变才会变,也只要文件变就会变, 也没有精确时间的限制,只要文件一遍,立马E-tag就不一样了 如果同时带有E-tag和Last-Modified,服务端会优先检查E-tag
各大缓存头部的整体关系如下图
cookie
Cookie是什么? Cookie 是一小段文本信息,伴随着用户请求和页面在 Web 服务器和浏览器之间传递。Cookie 包含每次用户访问站点时 Web 应用程序都可以读取的信息。
为什么需要Cookie? 因为HTTP协议是无状态的,对于一个浏览器发出的多次请求,WEB服务器无法区分 是不是来源于同一个浏览器。所以,需要额外的数据用于维护会话。 Cookie 正是这样的一段随HTTP请求一起被传递的额外数据。
Cookie能做什么? Cookie只是一段文本,所以它只能保存字符串。而且浏览器对它有大小限制以及 它会随着每次请求被发送到服务器,所以应该保证它不要太大。 Cookie的内容也是明文保存的,有些浏览器提供界面修改,所以, 不适合保存重要的或者涉及隐私的内容。
Cookie 的限制。 大多数浏览器支持最大为 4096 字节的 Cookie。由于这限制了 Cookie 的大小,最好用 Cookie 来存储少量数据,或者存储用户 ID 之类的标识符。用户 ID 随后便可用于标识用户,以及从数据库或其他数据源中读取用户信息。 浏览器还限制站点可以在用户计算机上存储的 Cookie 的数量。大多数浏览器只允许每个站点存储 20 个 Cookie;如果试图存储更多 Cookie,则最旧的 Cookie 便会被丢弃。有些浏览器还会对它们将接受的来自所有站点的 Cookie 总数作出绝对限制,通常为 300 个。
通过前面的内容,我们了解到Cookie是用于维持服务端会话状态的,通常由服务端写入,在后续请求中,供服务端读取。
常用的使用场景:
在登陆页面,用户登陆了
此时,服务端会生成一个session,session中有对于用户的信息(如用户名、密码等)
然后会有一个sessionid(相当于是服务端的这个session对应的key)
然后服务端在登录页面中写入cookie,值就是:jsessionid=xxx
然后浏览器本地就有这个cookie了,以后访问同域名下的页面时,自动带上cookie,自动检验,在有效时间内无需二次登陆。
复制代码
上述就是cookie的常用场景简述(当然了,实际情况下得考虑更多因素)
一般来说,cookie是不允许存放敏感信息的(千万不要明文存储用户名、密码),因为非常不安全,如果一定要强行存储,首先,一定要在cookie中设置httponly(这样就无法通过js操作了),另外可以考虑rsa等非对称加密(因为实际上,浏览器本地也是容易被攻克的,并不安全)
另外,由于在同域名的资源请求时,浏览器会默认带上本地的cookie,针对这种情况,在某些场景下是需要优化的。
譬如以下场景:
客户端在域名A下有cookie(这个可以是登陆时由服务端写入的)
然后在域名A下有一个页面,页面中有很多依赖的静态资源(都是域名A的,譬如有20个静态资源)
此时就有一个问题,页面加载,请求这些静态资源时,浏览器会默认带上cookie
也就是说,这20个静态资源的http请求,每一个都得带上cookie,而实际上静态资源并不需要cookie验证
此时就造成了较为严重的浪费,而且也降低了访问速度(因为内容更多了)
复制代码
当然了,针对这种场景,是有优化方案的(多域名拆分)。具体做法就是:
将静态资源分组,分别放到不同的域名下(如static.base.com) 而page.base.com(页面所在域名)下请求时,是不会带上static.base.com域名的cookie的,所以就避免了浪费 说到了多域名拆分,这里再提一个问题,那就是:
在移动端,如果请求的域名数过多,会降低请求速度(因为域名整套解析流程是很耗费时间的,而且移动端一般带宽都比不上pc) 此时就需要用到一种优化方案:dns-prefetch(让浏览器空闲时提前解析dns域名,不过也请合理使用,勿滥用) 关于cookie的交互,可以看下图总结:
长连接短链接
长连接:client方与server方先建立连接,连接建立后不断开,然后再进行报文发送和接收。这种方式下由于通讯连接一直存在。此种方式常用于P2P通信。
短连接:Client方与server每进行一次报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此方式常用于一点对多点通讯。C/S通信。
长连接与短连接的操作过程:
短连接的操作步骤是:建立连接——数据传输——关闭连接...建立连接——数据传输——关闭连接;
长连接的操作步骤是:建立连接——数据传输...(保持连接)...数据传输——关闭连接
长连接与短连接的使用时机:
长连接
连接多用于操作频繁,点对点的通讯,而且连接数不能太多的情况。每个TCP连接的建立都需要三次握手,每个TCP连接的断开要四次握手。如果每次操作都要 建立连接然后再操作的话处理速度会降低,所以每次操作下次操作时直接发送数据就可以了,不用再建立TCP连接。例如:数据库的连接用长连接,如果用短连接 频繁的通信会造成socket错误,频繁的socket创建也是对资源的浪费。
短连接
连接:web网站的http服务一般都用短连接。因为长连接对于服务器来说要耗费一定的资源。像web网站这么频繁的成千上万甚至上亿客户端的连接用短连 接更省一些资源。试想如果都用长连接,而且同时用成千上万的用户,每个用户都占有一个连接的话,可想而知服务器的压力有多大。所以并发量大,但是每个用户 又不需频繁操作的情况下需要短连接。
总之:长连接和短连接的选择要视需求而定。
http1.0/1.1/2.0
http1.0
早先1.0的HTTP版本,是一种无状态、无连接的应用层协议。
HTTP1.0规定浏览器和服务器保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。
这种无状态性可以借助cookie/session机制来做身份认证和状态记录。而下面两个问题就比较麻烦了。
首先,无连接的特性导致最大的性能缺陷就是无法复用连接。每次发送请求的时候,都需要进行一次TCP的连接,而TCP的连接释放过程又是比较费事的。这种无连接的特性会使得网络的利用率非常低。
其次就是队头阻塞(head of line blocking)。由于HTTP1.0规定下一个请求必须在前一个请求响应到达之前才能发送。假设前一个请求响应一直不到达,那么下一个请求就不发送,同样的后面的请求也给阻塞了。
为了解决这些问题,HTTP1.1出现了。
http1.1
对于HTTP1.1,不仅继承了HTTP1.0简单的特点,还克服了诸多HTTP1.0性能上的问题。
首先是长连接,HTTP1.1增加了一个Connection字段,通过设置Keep-Alive可以保持HTTP连接不断开,避免了每次客户端与服务器请求都要重复建立释放建立TCP连接,提高了网络的利用率。如果客户端想关闭HTTP连接,可以在请求头中携带Connection: false来告知服务器关闭请求。
注意: keep-alive不会永远保持,它有一个持续时间,一般在服务器中配置(如apache),另外长连接需要客户端和服务器都支持时才有效
其次,是HTTP1.1支持请求管道化(pipelining)。基于HTTP1.1的长连接,使得请求管线化成为可能。管线化使得请求能够“并行”传输。举个例子来说,假如响应的主体是一个html页面,页面中包含了很多img,这个时候keep-alive就起了很大的作用,能够进行“并行”发送多个请求。(注意这里的“并行”并不是真正意义上的并行传输,具体解释如下。)
需要注意的是,服务器必须按照客户端请求的先后顺序依次回送相应的结果,以保证客户端能够区分出每次请求的响应内容。
也就是说,HTTP管道化可以让我们把先进先出队列从客户端(请求队列)迁移到服务端(响应队列)。
http 2.0
http2.0不是https,它相当于是http的下一代规范(譬如https的请求可以是http2.0规范的)
然后简述下http2.0与http1.1的显著不同点:
http1.1中,每请求一个资源,都是需要开启一个tcp/ip连接的,所以对应的结果是,每一个资源对应一个tcp/ip请求,由于tcp/ip本身有并发数限制,所以当资源一多,速度就显著慢下来 http2.0中,一个tcp/ip请求可以请求多个资源,也就是说,只要一次tcp/ip请求,就可以请求若干个资源,分割成更小的帧请求,速度明显提升。 所以,如果http2.0全面应用,很多http1.1中的优化方案就无需用到了(譬如打包成精灵图,静态资源多域名拆分等)
然后简述下http2.0的一些特性:
多路复用(即一个tcp/ip连接可以请求多个资源)
首部压缩(http头部压缩,减少体积)
二进制分帧(在应用层跟传送层之间增加了一个二进制分帧层,改进传输性能,实现低延迟和高吞吐量)
服务器端推送(服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端)
请求优先级(如果流被赋予了优先级,它就会基于这个优先级来处理,由服务器决定需要多少资源来处理该请求。) 详细在这里
https
https中的s表示SSL或者TLS,就是在原http的基础上加上一层用于数据加密、解密、身份认证的安全层。
一般来说,主要关注的就是SSL/TLS的握手流程,如下(简述):
1. 浏览器请求建立SSL链接,并向服务端发送一个随机数–Client random和客户端支持的加密方法,比如RSA加密,此时是明文传输。
2. 服务端从中选出一组加密算法与Hash算法,回复一个随机数–Server random,并将自己的身份信息以证书的形式发回给浏览器
(证书里包含了网站地址,非对称加密的公钥,以及证书颁发机构等信息)
3. 浏览器收到服务端的证书后
- 验证证书的合法性(颁发机构是否合法,证书中包含的网址是否和正在访问的一样),如果证书信任,则浏览器会显示一个小锁头,否则会有提示
- 用户接收证书后(不管信不信任),浏览会生产新的随机数–Premaster secret,然后证书中的公钥以及指定的加密方法加密`Premaster secret`,发送给服务器。
- 利用Client random、Server random和Premaster secret通过一定的算法生成HTTP链接数据传输的对称加密key-`session key`
- 使用约定好的HASH算法计算握手消息,并使用生成的`session key`对消息进行加密,最后将之前生成的所有信息发送给服务端。
4. 服务端收到浏览器的回复
- 利用已知的加解密方式与自己的私钥进行解密,获取`Premaster secret`
- 和浏览器相同规则生成`session key`
- 使用`session key`解密浏览器发来的握手消息,并验证Hash是否与浏览器发来的一致
- 使用`session key`加密一段握手消息,发送给浏览器
5. 浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,
复制代码
详细这里
5 解析页面
前面有提到http交互,那么接下来就是浏览器获取到html,然后解析,渲染
流程简述
浏览器内核拿到内容后,渲染步骤大致可以分为以下几步:
1. 解析HTML,构建DOM树
2. 解析CSS,生成CSS规则树
3. 合并DOM树和CSS规则,生成render树
4. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
5. 绘制render树(paint),绘制页面像素信息
6. 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上
复制代码
如图:
html解析生成dom树
整个渲染步骤中,HTML解析是第一步。
简单的理解,这一步的流程是这样的:浏览器解析HTML,构建DOM树。
但实际上,在分析整体构建时,却不能一笔带过,得稍微展开。
解析HTML到构建出DOM当然过程可以简述如下:
Bytes → characters → tokens → nodes → DOM
譬如假设有这样一个HTML页面:
复制代码
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
<title>Critical Path</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
</body>
</html>
复制代码
浏览器的处理如下:
重点过程:
1. Conversion转换:浏览器将获得的HTML内容(Bytes)基于他的编码转换为单个字符
2. Tokenizing分词:浏览器按照HTML规范标准将这些字符转换为不同的标记token。每个token都有自己独特的含义以及规则集
3. Lexing词法分析:分词的结果是得到一堆的token,此时把他们转换为对象,这些对象分别定义他们的属性和规则
4. DOM构建:因为HTML标记定义的就是不同标签之间的关系,这个关系就像是一个树形结构一样
例如:body对象的父节点就是HTML对象,然后段略p对象的父节点就是body对象
复制代码
css解析生成样式树
同理,CSS规则树的生成也是类似。简述为:
Bytes → characters → tokens → nodes → CSSOM
譬如style.css内容如下:
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
复制代码
那么最终的CSSOM树就是:
渲染
渲染树
当DOM树和CSSOM都有了后,就要开始构建渲染树了
一般来说,渲染树和DOM树相对应的,但不是严格意义上的一一对应
因为有一些不可见的DOM元素不会插入到渲染树中,如head这种不可见的标签或者display: none等
整体来说可以看图:
渲染流程
图中重要的四个步骤就是:
1. 计算CSS样式
2. 构建渲染树
3. 布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性
4. 绘制,将图像绘制出来
复制代码
然后,图中的线与箭头代表通过js动态修改了DOM或CSS,导致了重新布局(Layout)或渲染(Repaint)
这里Layout和Repaint的概念是有区别的:
Layout,也称为Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树 Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了 回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流, 所以优化方案中一般都包括,尽量避免回流。
什么会引起回流?
1.页面渲染初始化
2.DOM结构改变,比如删除了某个节点
3.render树变化,比如减少了padding
4.窗口resize
5.最复杂的一种:获取某些属性,引发回流,
很多浏览器会对回流做优化,会等到数量足够时做一次批处理回流,
但是除了render树的直接变化,当获取一些属性时,浏览器为了获得正确的值也会触发回流,这样使得浏览器优化无效,包括
(1)offset(Top/Left/Width/Height)
(2) scroll(Top/Left/Width/Height)
(3) cilent(Top/Left/Width/Height)
(4) width,height
(5) 调用了getComputedStyle()或者IE的currentStyle
复制代码
回流一定伴随着重绘,重绘却可以单独出现
所以一般会有一些优化方案,如:
减少逐项更改样式,最好一次性更改style,或者将样式定义为class并一次性更新 避免循环操作dom,创建一个documentFragment或div,在它上面应用所有DOM操作,最后再把它添加到window.document 避免多次读取offset等属性。无法避免则将它们缓存到变量 将复杂的元素绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高
简单层与复合层
简单介绍下:
可以认为默认只有一个复合图层,所有的DOM节点都是在这个复合图层下的 如果开启了硬件加速功能,可以将某个节点变成复合图层 复合图层之间的绘制互不干扰,由GPU直接控制 而简单图层中,就算是absolute等布局,变化时不影响整体的回流,但是由于在同一个图层中,仍然是会影响绘制的,因此做动画时性能仍然很低。而复合层是独立的,所以一般做动画推荐使用硬件加速
详细简单层与复合层
js引擎解析
JS的解释阶段 首先得明确: JS是解释型语音,所以它无需提前编译,而是由解释器实时运行
引擎对JS的处理过程可以简述如下:
1. 读取代码,进行词法分析(Lexical analysis),然后将代码分解成词元(token)
2. 对词元进行语法分析(parsing),然后将代码整理成语法树(syntax tree)
3. 使用翻译器(translator),将代码转为字节码(bytecode)
4. 使用字节码解释器(bytecode interpreter),将字节码转为机器码
最终计算机执行的就是机器码。
复制代码
为了提高运行速度,现代浏览器一般采用即时编译(JIT-Just In Time compiler)
即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存(inline cache)
这样整个程序的运行速度能得到显著提升。
而且,不同浏览器策略可能还不同,有的浏览器就省略了字节码的翻译步骤,直接转为机器码(如chrome的v8)
总结起来可以认为是: 核心的JIT编译器将源码编译成机器码运行
JS的预处理阶段 上述将的是解释器的整体过程,这里再提下在正式执行JS前,还会有一个预处理阶段 (譬如变量提升,分号补全等)
预处理阶段会做一些事情,确保JS可以正确执行,这里仅提部分:
分号补全
JS执行是需要分号的,但为什么以下语句却可以正常运行呢?
console.log('a')
console.log('b')
复制代码
原因就是JS解释器有一个Semicolon Insertion规则,它会按照一定规则,在适当的位置补充分号
譬如列举几条自动加分号的规则:
当有换行符(包括含有换行符的多行注释),并且下一个token没法跟前面的语法匹配时,会自动补分号。 当有}时,如果缺少分号,会补分号。 程序源代码结束时,如果缺少分号,会补分号。 于是,上述的代码就变成了
console.log('a');
console.log('b');
复制代码
所以可以正常运行
当然了,这里有一个经典的例子:
function b() {
return
{
a: 'a'
};
}
复制代码
由于分号补全机制,所以它变成了:
function b() {
return;
{
a: 'a'
};
}
复制代码
所以运行后是undefined
变量提升
一般包括函数提升和变量提升
譬如:
a = 1;
b();
function b() {
console.log('b');
}
var a;
复制代码
经过变量提升后,就变成:
function b() {
console.log('b');
}
var a;
a = 1;
b();
复制代码
这里没有展开,其实展开也可以牵涉到很多内容的
譬如可以提下变量声明,函数声明,形参,实参的优先级顺序,以及es6中let有关的临时死区等
JS的执行阶段
这里可以参考深入理解javascript系列(10) 大叔讲解的十分到位
回收机制
JS有垃圾处理器,所以无需手动回收内存,而是由垃圾处理器自动处理。
一般来说,垃圾处理器有自己的回收策略。
譬如对于那些执行完毕的函数,如果没有外部引用(被引用的话会形成闭包),则会回收。(当然一般会把回收动作切割到不同的时间段执行,防止影响性能)
常用的两种垃圾回收规则是:
标记清除, 引用计数
Javascript引擎基础GC方案是(simple GC):mark and sweep(标记清除),简单解释如下:
遍历所有可访问的对象。 回收已不可访问的对象。 譬如:(出自javascript高程)
当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。 而当变量离开环境时,则将其标记为“离开环境”。 垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。 然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包,也就是说在环境中的以及相关引用的变量会被去除标记)。 而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。 最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。 关于引用计数,简单点理解:
跟踪记录每个值被引用的次数,当一个值被引用时,次数+1,减持时-1,下次垃圾回收器会回收次数为0的值的内存(当然了,容易出循环引用的bug)
GC的缺陷
和其他语言一样,javascript的GC策略也无法避免一个问题: GC时,停止响应其他操作
这是为了安全考虑。
而Javascript的GC在100ms甚至以上
对一般的应用还好,但对于JS游戏,动画对连贯性要求比较高的应用,就麻烦了。
这就是引擎需要优化的点: 避免GC造成的长时间停止响应。
GC优化策略
这里介绍常用到的:分代回收(Generation GC)
目的是通过区分“临时”与“持久”对象:
多回收“临时对象”区(young generation)
少回收“持久对象”区(tenured generation)
减少每次需遍历的对象,从而减少每次GC的耗时。
像node v8引擎就是采用的分代回收(和java一样,作者是java虚拟机作者。)
结语
本文仅是梳理下知识体系骨架,便于大家记忆,形成自己的知识树,开支阔叶。学海无涯,砥砺前行,不负初心!
再次感谢大佬们的分享,顺便提一下上篇的小问题 我给的答案是 8 只,不知道是不是最少的,请大佬们指教。