浏览器输入URL到页面加载完成

从输入URL到页面加载完成的过程

主干流程

  1. 浏览器接收url,开启一个新的网络请求线程(这部分涉及到浏览器进程与线程之间的关系)。
  2. 从开启网络请求线程到发起一个http请求(这部分涉及到dns解析、五层因特网协议栈、tcp/ip协议等)。
  3. 从服务器接收到一个请求到后台收到请求(这部分涉及到负载均衡、安全拦截、后台程序内部处理等知识)。
  4. 服务端和浏览器之间的交互(如http请求头、状态码、请求报文、cookie、session等)。
  5. http缓存(包括强缓存、协商缓存等)。
  6. CSS的可视化模型解析(元素的渲染规则、包含块、控制块、BFC、IFC等)。
  7. JS的解析过程(JS的解释过程、预处理过程、执行阶段的GO、VO、AO、作用域和作用域连等)。
  8. 其它(如安全策略、跨域等)

从浏览器接收url到开启网络请求线程

多进程的浏览器

浏览器是多进程的,有一个主控进程,每一个tab页都会新开一个进程。
这些进程可能包括:主控进程、插件进程、GPU、tab页等。

  • Browser进程:浏览器的主进程,负责调控,只有一个。
  • GPU进程:负责3D渲染,最多只有一个。
  • 第三方插件进程:负责管理第三方插件,每一个插件都有一个对应的进程,仅当该插件创建时创建。
  • 浏览器渲染进程(浏览器内核):默认每个tab页一个进程,互不干扰,负责控制页面渲染、脚本执行、事件处理等。

浏览器内核的五大线程

每一个tab页都可以看作一个浏览器内核进程,而该进程包含多个线程:

  • GUI线程:负责页面渲染、重绘、CSS解析等。
  • JS引擎线程:负责程序脚本的解析和执行。
  • 事件触发线程:负责管理事件循环,管理事件任务队列。
  • 定时触发器线程:setTImeout 和 setInterval 所在的线程。定时器的计时不是有JS引擎控制,而是由该线程控制。
  • http请求线程:负责http请求。

QA

Q:为什么JS引擎线程和GUI线程互斥?
A:因为JS是可以操作DOM的,如果在JS操作DOM的同时,GUI线程也在渲染页面,那么元素最终的呈现可能就不会和我们预期的一致了。
Q:为什么JS引擎是单线程?
A:1、创建JavaScript语言的时候,多进程多线程并不流行,硬件支持度不高。
2、多进程多线程使用时需要加锁,实施成本高。
3、如果多个线程同时操作一个DOM元素,可能元素的最终呈现不会如预期一样。
4、线程之间资源共享。
具体可以参考这篇文章

解析URL

当用户输入URL之后,会进行解析(URL的本质是统一资源标识符)。
URL一般包含这几部分:

  • protocol:协议头,如http、https 、ftp等。
  • host:指定主机域名或者IP地址。
  • port:端口号。
  • path:资源路径。
  • query:查询参数。
  • fragment:即#之后的hash值,一般用来定位到某个位置。

网络请求都是单独的线程

每次网络请求都会开辟一个新的线程进行,譬如如果URL解析到http协议,就会新建一个网络线程去处理资源下载,
因此浏览器会根据解析到的http协议去单独开辟出新的网络线程,前往请求资源。

从开启网络线程到发起http请求

这一部分主要包含dns解析、tcp/ip请求构建、五层因特网协议栈等。

DNS解析

如果用户输入的是域名,那么就需要进行dns解析获取对应的IP地址,具体步骤如下:

  1. 如果浏览器有缓存,就使用浏览器缓存;否则使用本机缓存,再没有就使用host。
  2. 如果本地没有缓存,就向dns服务器查询,(查询中可能会使用路由等,路由也是有缓存的,如果有,则使用),获取IP地址。

注意:域名查询可能经过dns调度器的。而且dns查询是很耗时,如果解析域名较多,那么网站的首屏加载就会变得很慢,这时可以考试使用dns-prefetch,这一点请参考这里

TCP/IP请求

http请求本质上还是tcp/ip请求。
tcp将http长报文划分为短报文,通过三次握手与服务器建立连接,进行可靠传输。

三次握手

image.png
TCP报头中的源端口号和目的端口号同IP数据报中的源IP与目的IP唯一确定一条TCP连接。TCP在发送数据前必须在彼此间建立连接,这里连接意思是:双方需要内保存对方信息(例如:IP,Port…)。

报文主要段的意思:

  • 序号seq:发送的数据字节流,确保TCP连接传输有序,对每个字节编号。序列号seq就是这个报文段中的第一个字节的数据编号。
  • 确认序号ack:发送方期待接收的下一序列号,接收成功后序列号加1,只有ACK为1时,才有效。
  • 确认ACK:仅当ACK为1时确认号才有效,为0时无效。
  • 同步SYN:连接建立时用于同步序号。当SYN=1,ACK=0时,指的是这是一个请求报文段,若统一连接,则SYN=1,ACK=1。因此,SYN=1,表示这是一个请求连接,或者连接接收报文。SYN只有在建立连接时才会被置为1,握手结束之后置为0.
  • 终止FIN:用于释放连接。当FIN=1时,表示此报文段的发送方的数据已经发送完毕,并要求释放连接。

PS:ACK、SYN和FIN这些大写的单词表示标志位,其值要么是1,要么是0;ack、seq小写的单词表示序号。

字段 含义
URG 紧急指针是否有效。为1,表示某一位需要被优先处理
ACK 确认号是否有效,一般置为1。
PSH 提示接收端应用程序立即从TCP缓冲区把数据读走。
RST 对方要求重新建立连接,复位。
SYN 请求建立连接,并在其序列号的字段进行序列号的初始值设定。建立连接,设置为1
FIN 希望断开连接。

image.png

  1. 第一次握手:建立连接时,客户端发送初始序号seq=x,SYN=1请求标志。让服务器知道客户端发送,自己接收正常。
  2. 第二次握手:服务器发送请求标志SYN,发送确认标志ACK,服务器自己的序号seq,发送客户端的确认标志ack:SYN=1,ACK=1,seq=y,ack=x + 1。让客户端知道自己接收、发送正常,服务器接收、发送正常。
  3. 第三次握手:客户端发送确认标志ACK,自己的序号seq,发送服务端的确认序号ack:ACK=1,seq=x+1,ack=y+1。让服务器知道客户端发送、接收,自己发送、接收正常。

从上面可知,三次握手是让服务器和客户端知道彼此双方发送、接收能力正常的最少握手次数。

四次挥手

image.png

四次挥手过程解析:

  1. 第一次挥手:客户端主动请求断开,向服务端发送FIN=1,seq=u,进入FIN-WAIT-1状态。
  2. 第二次挥手:服务端接收到客户端发送的信息,发送确认序号ACK=1,客户端的确认号ack=u+1,自己的序号seq=v。并且进入CLOSE-WAIT状态。
  3. 第三次挥手:客户端接收到服务端的信息后,进入FIN-WAIT-2状态。此时服务器发送释放FIN=1信号,发送确认标志ACK=1,自己的序号seq=w,客户端的确认号ack=u+1。并且自己进入LAST-ACT状态(最好确认状态)。
  4. 第四次挥手:客户端收到服务端信息后,发送确认标志ACK=1,自己的序号seq=u+1,服务端的确认号ack=w+1。客户端进入TIME-WAIT状态。等待2MSL(2个最长报文段寿命)之后,进入CLOSE状态。服务端收到确认后,也进入CLOSE状态。

QA

Q:为什么三次握手和四次挥手?
A:三次握手时,服务器同时把ACK和SYN放在一起发送到了客户端那里。四次挥手时,当收到对方的 FIN 报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方是否现在关闭发送数据通道,需要上层应用来决定,因此,己方 ACK 和 FIN 一般都会分开发送。
Q:为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
A:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
Q:如果已经建立了连接,但是客户端突然出现故障了怎么办?
A:TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

tcp/ip的并发限制

浏览器针对同一个域名下的tcp连接是有限制的(一般是5-20个)。而且在http1.0中,一个资源下载就对应着一个tcp/ip连接。

get和post的区别

get和post本质上都是tcp/ip,但是两者除了在http层面上的不同,在tcp/ip层面也有区别。
get会产生一个tcp包,而post会产生两个:

  • get请求时会把headers和data一起发送出去,服务器响应200。
  • post请求会先把headers发送出去,服务器响应100 continue,然后浏览器在发送data,服务器响应200。

其他区别:

分类 GET POST
可否被缓存 可以被缓存 不可以被缓存
后退或刷新 无害 重新提交请求
编码类型 application/x-www-form-urlencoded application/x-www-form-urlencoded 或 multipart/form-data。为二进制数据使用多重编码。
历史 会被保留在浏览器历史记录中 不会被保留在浏览器历史记录中
对数据类型的限制 只允许ASCII字符 无限制
安全性 与POST比,安全性较差,因为发送的数据会暴露在地址栏中 POST比GET较安全
可见性 可见 不可见

五层因特网协议

从客户端发出http请求到服务器接收,这中间会经过一系列的过程:
从应用层发出http请求,到传输层经过三次握手建立tcp连接,再到网络层的IP寻址,再到数据链路层封装成帧,最后经由物理层传输。
五层因特网协议栈其实就是:

  1. 应用层(dns http)DNS解析IP并发送http请求。
  2. 传输层(tcp udp)经过三次握手,建立tcp连接。
  3. 网络层(IP ARP)IP寻址。
  4. 数据链路层(PPP)封装成帧。
  5. 物理层(利用无力介质传输比特流)物理传输(然后传输的时候通过双绞线,电磁波等介质)。

相比于完整的IOS/OSI七层框架,少了表示层和会话层。
OSI七层框架:物理层数据链路层网络层传输层会话层表示层应用层

  1. 表示层:主要处理两个通讯系统之间的信息交换的表示方式,包括数据格式交换、数据加密解密等。
  2. 会话层:它管理不同用户的进程之间的对话,如登录、注销等。

从服务器接收请求到后台接收到请求

负载均衡

对于一个大型项目来说,它的并发访问量可能会很大,所以如果只有一台服务器可能是吃不消的,所以一般会有若干个服务器组成一个集群,然后配合反向代理服务器实现负载均衡。
简单来说:就是用户的请求都会指向一个反向代理服务器,如nginx,然后代理服务器按照指定的算法,分配不同的请求给对应集群中的服务器执行,然后反响代理服务器等待服务器的http响应,并将其反应给用户。
**

后台处理

一般后台都是部署到一个容器中,所以:

  • 先是容器接收到请求(如tomcat),
  • 然后对应容器中的程序接收到请求(如java、php等),
  • 然后就是后台程序中的内部处理,处理完成之后响应结果。

一般后台都会有统一的验证:安全拦截、跨域处理等,如果验证不通过,则直接拒绝请求。如果验证通过,才会进入后台程序,后台程序进行对应的操作,如数据查询等。等程序执行完毕之后,会返回一个http包,然后将这个包发送给前端,完成交互。

后台和前台的交互

前后端交互的时候,http报文作为最重要的信息载体。

http报文结构

报文一般包括了通用头部、请求/响应头部、请求/响应体

通用头部
  • Request Url:请求的web服务器地址。
  • Request Method:请求方式(GET、POST、PUT、DELETE、HEAD、CONNECT、TRACE)。
  • Status Code:请求返回的状态码。
  • Remote Address:请求的远程服务器地址(会被转为IP)。

其中methods一般可以分为两批次:

  • HTTP1.0定义了三种请求方法:GET、POST和HEAD方法以及几种额外的请求方式(PUT、DELETE、LINK和UNLINK)。HTTP1.0具体定义请参考:https://tools.ietf.org/html/rfc1945
  • HTTP1.1定义了八种请求方式:GET、POST、PUT、DELETE、HEAD、CONNECT、TRACE。HTTP1.1具体定义请参考:https://tools.ietf.org/html/rfc2616
常用状态码
200-请求成功。请求的资源已返回客户端。
301-永久重定向。
302-临时重定向。
304-表明自上次请求之后,资源未做变更,请使用本地缓存的资源。
400-客户端请求出错。
401-请求未经授权。
403-禁止访问。
404-请求资源不存在。
500-服务器内部错误。
503-服务器不可用。
......

最后列举下大致不同范围的状态码:

1xx:指示信息,表示请求已接受,正在处理。
2xx:请求已被成功接收。
3xx:重定向,要完成请求需要进一步的操作。
4xx:客户端错误,请求语法错误或者请求无法实现。
5xx:服务端错误,服务器实现合法的请求。

请求/响应头部

常用的请求头部:

Accept:接收类型。表示浏览器可以支持的MIME类型(对标服务器返回的Content-Type)。
Accept-Encodeing:浏览器支持的压缩类型,如GZip,超出类型不能接收。
ConContent-Type:客户端发出去的实体内容的类型。
Cache-Control:指定请求和响应应遵循的缓存机制,如no-cache等。
If-Modifiy-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:请求的服务器地址。
Origin:请求最初从哪里发起的。Origin比Referer更尊重隐私。
Referer:该页面的来源URL(CSRF拦截常用该字段)。
User-Agent:用户客户端的信息。

常用的响应头部:

Access-Control-Allow-Headers:服务器允许的请求headers。
Access-Control-Allow-Methods:服务器允许的请求方式。
Access-Control-Allow-Methods:服务器允许的请求Origin头部(如*)。
Content-Type:服务器返回的响应内容实体的类型。
Date:数据从服务器返回的时间。
Cache-Control:告诉浏览器或其他客户,什么环境可以安全的缓存文档。
Last-Modified:请求资源最后的修改时间。
Expires:告诉浏览器应该在什么时候放弃使用资源,从而不在缓存它。
Max-age:告诉浏览器应该缓存多长时间,开启Cache-Control有效。
ETag:请求资源的文件指纹。
Set-cookie:设置该页面的cookie,服务器通过这个设置浏览器页面的cookie。
Keep-Alive:如果浏览器有keep-alive,那么服务器也有对应的keep-alive。
Server:服务器的一些信息。

一般来说,请求头部和响应头部都是匹配分析的
如:请求头部的Accept要和响应头部的Content-Type是要匹配的,否则就会报错。
如:跨域请求时,请求头部的Origin要和响应头部的Access-Control-Allow-Origin匹配,否则回报跨域错误。
再比如:缓存时,请求头部的If-Modified-Since和ETag,要与响应头部的Last-Modified和If-None-Match相匹配。

请求和响应实体

http请求时,除了头部,还有消息实体,一般来说
请求实体中会将一些需要的参数都放入进入(用于post请求)。
譬如实体中可以放参数的序列化形式(a=1&b=2这种),或者直接放表单对象(Form Data对象,上传时可以夹杂参数以及文件),等等
而一般响应实体中,就是放服务端需要传给客户端的内容
一般现在的接口请求时,实体中就是对于的信息的json格式,而像页面请求这种,里面就是直接放了一个html字符串,然后浏览器自己解析并渲染。
image.png

cookie及相关优化

cookie 是一种本地存储方式,主要用于浏览器和服务器通信,常用于验证身份,和服务端的session搭配使用。
场景如下(简述):

在登陆页面,用户登陆了
此时,服务端会生成一个session,session中有对于用户的信息(如用户名、密码等)
然后会有一个sessionid(相当于是服务端的这个session对应的key)
然后服务端在登录页面中写入cookie,值就是:jsessionid=xxx
然后浏览器本地就有这个cookie了,以后访问同域名下的页面时,自动带上cookie,自动检验,在有效时间内无需二次登陆

上述就是cookie的常用场景简述(当然了,实际情况下得考虑更多因素)。
一般来说,cookie中是不允许存放敏感信息的,如帐号,密码等。如果非要存储,一定要在cookie 中这只httpOnly,这样就无法通过js操作获取cookie了。
另外,在同域名操作时,总会默认带上该域名下所有的cookie。针对这种情况,在某些场景中是需要优化的:

比如以下场景:

客户端在域名A下有cookie(这个可以是登陆时由服务端写入的)
然后在域名A下有一个页面,页面中有很多依赖的静态资源(都是域名A的,譬如有20个静态资源)
此时就有一个问题,页面加载,请求这些静态资源时,浏览器会默认带上cookie
也就是说,这20个静态资源的http请求,每一个都得带上cookie,而实际上静态资源并不需要cookie验证
此时就造成了较为严重的浪费,而且也降低了访问速度(因为内容更多了)

针对这种场景,我们可以使用域名拆分的方案:将静态资源分组,分别存放到不同的服务器中。
而在移动端,如果请求的域名过多时,会降低请求速度(因为域名解析是很耗时的,而且移动端的带宽大都不如pc),这时我们可以使用dns-prefetch,让浏览器空闲时提前解析域名。
关于cookie 的操作大致可以如下图所示:
image.png

gzip压缩

首先,明确gzip是一种压缩格式,需要浏览器支持才有效(不过一般现在浏览器都支持),
而且gzip压缩效率很好(高达70%左右)
然后gzip一般是由apachetomcat等web服务器开启
当然服务器除了gzip外,也还会有其它压缩格式(如deflate,没有gzip高效,且不流行)
所以一般只需要在服务器上开启了gzip压缩,然后之后的请求就都是基于gzip压缩格式的,
非常方便。

长连接与短连接

首先看tcp/ip层面的定义:

  • 长连接:一个tcp/ip连接上可以连续发送多个数据包,在tcp连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持(类似于心跳包)
  • 短连接:通信双方有数据交互时,就建立一个tcp连接,数据发送完成后,则断开此tcp连接

然后在http层面:

  • http1.0中,默认使用的是短连接,也就是说,浏览器没进行一次http操作,就建立一次连接,任务结束就中断连接,譬如每一个静态资源请求时都是一个单独的连接
  • http1.1起,默认使用长连接,使用长连接会有这一行Connection: keep-alive,在长连接的情况下,当一个网页打开完成后,客户端和服务端之间用于传输http的tcp连接不会关闭,如果客户端再次访问这个服务器的页面,会继续使用这一条已经建立的连接。

注意: keep-alive不会永远保持,它有一个持续时间,一般在服务器中配置(如apache),另外长连接需要客户端和服务器都支持时才有效。

http2.0

http2.0不是https,它是http的下一代规范。
简述一下http2.0和http1.1的区别:

  • http1.1中,每请求一个资源,都需要新开一个tcp/ip连接,所以对应的结果就是每一个资源都占用一个tcp/ip请求,而tcp/ip请求并发数量是有限制的,当请求资源数量一多,请求速度就会慢下来。
  • http2.0中,一个tcp/ip请求可以请求多个资源,也就是说,只要一次tcp/ip请求,就可以请求诺干个资源,分割成更小的帧请求,速度会有显著提升。

所以,如果http2.0得到全面的使用,那么http1.1中的很多优化手段就不必在使用了,比如:使用雪碧图,静态资源多域名拆分等。
http2.0的特性:

  1. 多路复用(即一个tcp/ip请求可以请求多个资源)。
  2. 首部压缩(http头部压缩,减少体积)。
  3. 二进制分帧(在应用层和传输层使用二进制分帧,改善传输性能,实现低延迟和高吞吐量)。
  4. 服务器推送(服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端)。
  5. 请求优先级(如果流被赋予了优先级,那么它就会基于这个优先级被处理,且由服务器决定使用多少资源来处理这个流)。

https

https是安全版本的http。譬如一些支付等操作都是基于https的,http的安全性太低。
简单来说,https和http的区别就是在请求建立连接之前,先建立ssl连接,确保之后的通信都是加密的,无法被轻易截取分析。
一般来说,如果要将网站升级成https,需要后端支持(后端需要申请证书等),然后https的开销也比http要大(因为需要额外建立安全链接以及加密等),所以一般来说http2.0配合https的体验更佳(因为http2.0更快了)。

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一致,此时握手过程结束。

之后所有的https通信数据将由之前浏览器生成的session key并利用对称加密算法进行加密。
image.png

图片来源于阮一峰的图解SSL/TSL协议

http缓存

缓存可以被简单的划分为两类:强缓存(200 from cache)和协商缓存(304)。
区别:

  • 强缓存:如果浏览器判断本地缓存未过期的时候,将会直接使用本地缓存,不会发起请求。
  • 协商缓存:浏览器会向服务器发起请求,在请求头部携带资源文件的缓存信息,如If-Modified-Since、ETag等,由服务器端判断,资源自从上次访问是否发生过变更,如果发生过变更,则返回状态码200,并将新的资源文件和缓存规则返回;如果未变更,返回304状态码,告诉浏览器使用本地缓存。

缓存头部概述

上面提到的强缓存和协商缓存,那么它们是如何进行区分的呢?
答案是通过不同的http头部控制。
这里列举一些常用的缓存头部:
用于强缓存控制的:

  • http1.0: Pragma/Expires
  • http1.1: Cache-Control/Max-Age

注意:Max-Age不是一个头部,它是Cache-Control头部的值。

用于协商缓存控制的:

  • http1.0: Last-Modified/If-Modified-Since
  • http1.1:E-tag/If-None-Match

再提一点,其实HTML页面中也有一个meta标签可以控制缓存方案-Pragma

<META HTTP-EQUIV="Pragma" CONTENT="no-cache">

不过,这种方案还是比较少用到,因为支持情况不佳,譬如缓存代理服务器肯定不支持,所以不推荐。

头部的区别

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。它的作用是,当服务器接收到请求时,如果二者匹配,则说明服务端资源未改变,可以使用本地缓存,只返回头部,状态码304。
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-MatchE-tag匹配,则代表内容未变,通知浏览器使用本地缓存,和Last-Modified不同,E-tag更精确,它是类似于指纹一样的东西,基于FileEtag INode Mtime Size生成,也就是说,只要文件变,指纹就会变,而且没有1s精确度的限制。

Max-Age和Expires

Expires使用的是服务器端的时间
但是有时候会有这样一种情况-客户端时间和服务端不同步
那这样,可能就会出问题了,造成了浏览器本地的缓存无用或者一直无法过期
所以一般http1.1后不推荐使用Expires
Max-Age使用的是客户端本地时间的计算,因此不会有这个问题
因此推荐使用Max-Age
注意,如果同时启用了Cache-ControlExpiresCache-Control优先级高。

E-tag和Last-Modified

Last-Modified

  • 表明服务端的文件最后何时改变的
  • 它有一个缺陷就是只能精确到1s,
  • 然后还有一个问题就是有的服务端的文件会周期性的改变,导致缓存失效

E-tag

  • 是一种指纹机制,代表文件相关指纹
  • 只有文件变才会变,也只要文件变就会变,
  • 也没有精确时间的限制,只要文件一遍,立马E-tag就不一样了

如果同时带有E-tagLast-Modified,服务端会优先检查E-tag
各大缓存头部的整体关系如下图:
image.png

解析页面

前面有提到http交互,那么接下来就是浏览器获取到html,然后解析,渲染。

流程简述

浏览器内核拿到内容后,渲染步骤大致分为一下几步:

1、解析HTML,构建DOM Tree
2、解析CSS,构建CSS Rules Tree
3、合并DOM Tree 和 CSS Rules Tree(其实是将CSS Rules Tree附在DOM Tree之上),生成render Tree。
4、布局人的人 Tree(Layout/reflow),负责计算各元素的尺寸和位置的计算。
5、绘制render Tree(paint),绘制页面像素信息。
6、浏览器将各层的信息发送给GPU,由GPU将各层合层(composite),显示在屏幕上。

如下图:
image.png

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>

浏览器的处理如下:
image.png
列举其中的一些重点过程:

  1. Conversion转换:浏览器将获得的HTML内容(Bytes)基于它的编码格式转换为字符串。
  2. Tokeizing分词:浏览器按照HTML规范将这些字符串转换未不同的标记token,每一个token都有自己独特的含义和规则。
  3. Lexing词法分析:分词的结果就是获得一堆token,此时将他们转换为对象,为他们定义它们的属性和规则。
  4. DOM构建:因为HTML标记的就是不同标签之间的关系,这个关系就像树形结构一样。

最终的DOM树如下图所示:
image.png

生成CSS规则

同理,CSS规则树的生成也是类似。简述为:
Bytes → characters → tokens → nodes → CSSOM

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

最终生成的CSS Rules Tree:
image.png

构建渲染树

当DOM树和CSSOM都有了后,就要开始构建渲染树了
一般来说,渲染树和DOM树相对应的,但不是严格意义上的一一对应
因为有一些不可见的DOM元素不会插入到渲染树中,如head这种不可见的标签或者display: none
整体来说可以看图:
image.png

渲染

有了render树,接下来就是开始渲染,基本流程如下:
image.png

图中重要的四个步骤就是:

1. 计算CSS样式
2. 构建渲染树
3. 布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性
4. 绘制,将图像绘制出来

然后,图中的线与箭头代表通过js动态修改了DOM或CSS,导致了重新布局(Layout)或渲染(Repaint)。
这里Layout和Repaint的概念是有区别的:

  • Layout,也称为Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树
  • Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了

注意:回流的成本要远高于重绘,因为回流会造成同级元素和子节点的回流。所以,一定要尽可能的避免回流。

什么会造成回流

  • 页面渲染初始化
  • DOM结构改变,如删除某个节点
  • render Tree发生变化,如padding改变
  • 窗口resize
  • 最复杂的一种:获取元素的某个属性:
    • offsetTop/Left/Width/Height
    • scrollTop/Left/Width/Height
    • clientTop/Left/Width/Height
    • width/height
    • 调用了getComputedStyle()或者IE的currentStyle

回流一定会伴随着重绘,重绘却不一定造成回流,这是一个充分必要条件。

优化方案
  1. 减少逐项更改样式,最好一次性更改,或者使用定义样式class一次性更改。
  2. 避免循环操作dom,创建一个documentFragment或div,在它上面应用所有DOM操作,最后再把它添加到window.document。
  3. 避免多次调用读取offset等属性,必要的时候可以缓存到变量中。
  4. 将复杂的元素决定定位或者固定定位,使其脱离文档流,否则造成的回流代价太大。

注意:改变字体大小会引发回流

复合层与简单层

浏览器渲染的图层一般分为两大类:普通图层和复合图层。

  • DOM中每一个节点都对应一个普通图层。
  • 复合图层就是普通图层的合并。一个页面一般来说只要一个复合图层。

浏览器什么时候会创建复合图层

  • 3D 或者 CSS transform

  • CSS filters

  • 元素覆盖时,比如使用了 z-index 属性

如何变成复合图层

将一个元素变为复合图层,就是使用传说中的硬件加速。

  • transform:translate3d(0,0,0),或者translateZ(0)。
  • opacity属性/过度动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
  • will-chang属性(这个比较偏僻),一般配合opacity与translate使用(而且经测试,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层),will-change的作用就是告诉浏览器该元素会有那些发生变化的方法,让浏览器为该元素的变化做好优化准备,具体请移步

复合图层的作用

一般一个元素开启硬件加速后会变成复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能
但是尽量不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡。

使用硬件加速的问题

  1. 内存。如果GPU加载了大量的纹理,那么很容易就会发生内容问题,这一点在移动端浏览器上尤为明显,所以,一定要牢记不要让页面的每个元素都使用硬件加速。
  2. 使用GPU渲染会影响字体的抗锯齿效果。这是因为GPU和CPU具有不同的渲染机制。即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。

注意:使用硬件加速时,尽可能的使用index,防止浏览器默认给后续的元素创建复合层渲染。
具体的原理时这样的:
webkit CSS3中,如果这个元素添加了硬件加速,并且index层级比较低,
那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且releative或absolute属性相同的),
会默认变为复合层渲染,如果处理不当会极大的影响性能

简单点理解,其实可以认为是一个隐式合成的概念:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层,这点需要特别注意

QA

Q:为什么开启transform的元素不会引起回流和重绘?
A:因为transform属性支持的位移函数translate()、缩放比例函数scale()、斜切函数skew()、旋转函数rotate()都支持线性映射的形式,也就是matrix( )表示的方式,简单来说就是所有transform实现的效果都可以对原坐标系中的点[x,y]按照如下的齐次矩阵进行计算得到变换后的点坐标[x',y']
image.png
齐次矩阵的系数是设定transform变换时传入的,是一个已知项,而使用三维的齐次矩阵是因为二维坐标的点在变换时会产生常数项(主要是平移变换),而如果以二维矩阵作为参数来计算时,以x坐标变换为例,结果的形式就是x'=ax+by,其中是没有常量的,所以只能采用一个三维齐次矩阵来表示,但计算中的第三个坐标实际上并不需要使用。更多的关于变换的数学原理,感兴趣的读者可以自行查阅资料。
所以translform在动画过程中不是使用缓存,而是在图层合成时遍历当前层的点,然后利用上述方法计算出新的坐标点即可。它可以视为一种与图层内容无关的变换,图层中的元素首次生成的位图信息缓存可以被反复使用。比如一段平移动画,如果使用绝对定位+改变left值的方式来实现,就需要不断计算动画元素的布局并更新它的像素信息,但如果使用translate来实现,动画元素在文档流中的位置并不需要改变,无论后续平移到多远,都可以使用位图缓存中保存的初始位置信息,再加上变换矩阵的影响在层合并时计算出来,同样既不影响布局,也不需要重绘,这就是它高性能的原因。
Q:为什么使用opacity的元素不会引起回流和重绘?
A:opacity单词意思为透明度,直观视觉效果就是颜色变淡了,但最终显示的颜色其实仍然可以用RGB三个通道来表示,从数值运算的角度来看,它实际上表示了它采用一般混合策略和其他颜色进行混合时的比例,也就是:

例如在网页默认的白底色上rgba(255,255,255)显示一个包含透明度的rgba(218,89,97,0.8)颜色, 那么颜色的RGB分量都按照上述公式进行计算就得到rgb(225,122,128),用取色器拾取一下渲染出来的点,结果和上述理论是一致的:
image.png
所以opacity这个属性本身就是用在重叠部分颜色处理的过程中使用的,对于分层的图原来说就可以看作是与图层内容无关的系数,因为合成过程中当前层中所有像素都需要经历上面的颜色混合公式,所以opacity的动画过程既不会影响布局,也不需要重绘。这样图层中保存的RGB像素数据的缓存在动画过程中也就不需要更新了,如果不使用opacity属性的话,每一帧对于变化部分都需要手动重计算RGB颜色值(这也就相当于是重绘了),因为这些区域的像素颜色一直都在变化,缓存也就没有意义。现在再来看看opacity的性能优势,就相对容易理解了。

小结
opacitytransform动画的高性能是由于其数学原理决定了可以使用缓存信息,而并不是因为它被硬件加速了。

参考:高性能的Web动画和渲染原理
使用CSS开启硬件加速提高性能
复合图层和简单图层

Chrome中的调试

Chrome的开发者工具中,Performance中可以看到详细的渲染过程:
image.png
image.png

资源外链的下载

上面介绍了html解析,渲染流程。但实际上,在解析html时,会遇到一些资源连接,此时就需要进行单独处理了
简单起见,这里将遇到的静态资源分为一下几大类(未列举所有):

  • CSS样式资源
  • JS脚本资源
  • img图片类资源

当遇到外链时的处理

当遇到上述的外链时,会单独开启一个线程去下载资源(http1.1中每一个资源的下载都对应一个tcp/ip连接)。

遇到CSS资源外链

CSS资源的处理有几个特点:

  • CSS下载时异步,不会阻塞浏览器构建DOM树
  • 但是会阻塞渲染,也就是在构建render时,会等到css下载解析完毕后才进行(这点与浏览器优化有关,防止css规则不断改变,避免了重复的构建)
  • 有例外,media query声明的CSS是不会阻塞渲染的

遇到脚本资源

JS脚本资源的处理有几个特点:

  • 阻塞浏览器的解析,也就是说发现一个外链脚本时,需等待脚本下载完成并执行后才会继续解析HTML
  • 浏览器的优化,一般现代浏览器有优化,在脚本阻塞时,也会继续下载其它资源(当然有并发上限),但是虽然脚本可以并行下载,解析过程仍然是阻塞的,也就是说必须这个脚本执行完毕后才会接下来的解析,并行下载只是一种优化而已
  • defer与async,普通的脚本是会阻塞浏览器解析的,但是可以加上defer或async属性,这样脚本就变成异步了,可以等到解析完毕后再执行

注意,defer和async是有区别的: defer是延迟执行,而async是异步执行。
简单的说(不展开):

  • async是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在onload前,但不确定在DOMContentLoaded事件的前或后
  • defer是延迟执行,在浏览器看起来的效果像是将脚本放在了body后面一样(虽然按规范应该是在DOMContentLoaded事件前,但实际上不同浏览器的优化效果不一样,也有可能在它后面)

遇到图片资源

遇到图片等资源时,直接就是异步下载,不会阻塞解析,下载完毕后直接用图片替换原有src的地方

loaded和domcontentloaded

简单的对比:

  • DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片(譬如如果有async加载的脚本就不一定完成)。等同于$(document).ready()
  • load 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了

CSS的可视化模型

这一部分内容很多参考《精通CSS-高级Web标准解决方案》以及参考来源
前面提到了整体的渲染概念,但实际上文档树中的元素是按什么渲染规则渲染的,是可以进一步展开的,此部分内容即: CSS的可视化格式模型
先了解:

  • CSS中规定每一个元素都有自己的盒子模型(相当于规定了这个元素如何显示)
  • 然后可视化格式模型则是把这些盒子按照规则摆放到页面上,也就是如何布局
  • 换句话说,盒子模型规定了怎么在页面里摆放盒子,盒子的相互作用等等

说到底: CSS的可视化格式模型就是规定了浏览器在页面中如何处理文档树
关键字:

包含块(Containing Block)
控制框(Controlling Box)
BFC(Block Formatting Context)
IFC(Inline Formatting Context)
定位体系
浮动
...

另外,CSS有三种定位机制:普通流浮动绝对定位,如无特别提及,下文中都是针对普通流中的。

包含块(Containing Block)

一个元素的box的定位和尺寸,会与某一矩形框有关,这个框就称之为包含块。
元素会为它的子孙元素创建包含块,但是,并不是说元素的包含块就是它的父元素,元素的包含块与它的祖先元素的样式等有关系
譬如:

  • 根元素是最顶端的元素,它没有父节点,它的包含块就是初始包含块
  • static和relative的包含块由它最近的块级、单元格或者行内块祖先元素的内容框(content)创建
  • fixed的包含块是当前可视窗口
  • absolute的包含块由它最近的position 属性为absoluterelative或者fixed的祖先元素创建
    • 如果其祖先元素是行内元素,则包含块取决于其祖先元素的direction特性
    • 如果祖先元素不是行内元素,那么包含块的区域应该是祖先元素的内边距边界

控制框(Controlling Box)

块级元素和块框以及行内元素和行框的相关概念
块框:

  • 块级元素会生成一个块框(Block Box),块框会占据一整行,用来包含子box和生成的内容
  • 块框同时也是一个块包含框(Containing Box),里面要么只包含块框,要么只包含行内框(不能混杂),如果块框内部有块级元素也有行内元素,那么行内元素会被匿名块框包围

关于匿名块框的生成,示例:

<DIV>
Some text
  <P>More text</P>
</DIV>

div生成了一个块框,包含了另一个块框p以及文本内容Some text,此时Some text文本会被强制加到一个匿名的块框里面,被div生成的块框包含(其实这个就是IFC中提到的行框,包含这些行内框的这一行匿名块形成的框,行框和行内框不同)
换句话说:
如果一个块框在其中包含另外一个块框,那么我们强迫它只能包含块框,因此其它文本内容生成出来的都是匿名块框(而不是匿名行内框)
**行内框:

  • 一个行内元素生成一个行内框
  • 行内元素能排在一行,允许左右有其它元素

关于匿名行内框的生成,示例:

<P>Some <EM>emphasized</EM> text</P>

P元素生成一个块框,其中有几个行内框(如EM),以及文本Sometext,此时会专门为这些文本生成匿名行内框。
display属性的影响
display的几个属性也可以影响不同框的生成:

  • block,元素生成一个块框
  • inline,元素产生一个或多个的行内框
  • inline-block,元素产生一个行内级块框,行内块框的内部会被当作块块来格式化,而此元素本身会被当作行内级框来格式化(这也是为什么会产生BFC
  • none,不生成框,不再格式化结构中,当然了,另一个visibility: hidden则会产生一个不可见的框

总结:

  • 如果一个框里,有一个块级元素,那么这个框里的内容都会被当作块框来进行格式化,因为只要出现了块级元素,就会将里面的内容分块几块,每一块独占一行(出现行内可以用匿名块框解决)
  • 如果一个框里,没有任何块级元素,那么这个框里的内容会被当成行内框来格式化,因为里面的内容是按照顺序成行的排列。

BFC(Block Formatting Context)

块级格式化上下文。
FC即格式上下文,它定义框内部的元素渲染规则,比较抽象,譬如:

FC像是一个大箱子,里面装有很多元素
箱子可以隔开里面的元素和外面的元素(所以外部并不会影响FC内部的渲染)
内部的规则可以是:如何定位,宽高计算,margin折叠等等

不同类型的框参与的FC类型不同,譬如块级框对应BFC,行内框对应IFC
注意,并不是说所有的框都会产生FC,而是符合特定条件才会产生,只有产生了对应的FC后才会应用对应渲染规则
BFC规则:

在块格式化上下文中
每一个元素左外边与包含块的左边相接触(对于从右到左的格式化,右外边接触右边)
即使存在浮动也是如此(所以浮动元素正常会直接贴近它的包含块的左边,与普通元素重合)
除非这个元素也创建了一个新的BFC

总结几点BFC特点:

  1. 内部box在垂直方向,一个接一个的放置
  2. box的垂直方向由margin决定,属于同一个BFC的两个box间的margin会重叠
  3. BFC区域不会与float box重叠(可用于排版)
  4. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此
  5. 计算BFC的高度时,浮动元素也参与计算(不会浮动坍塌)

如何触发BFC?

  1. 根元素
  2. float属性不为none
  3. positionabsolutefixed
  4. displayinline-block, flex, inline-flextabletable-celltable-caption
  5. overflow不为visible

这里提下,display: table,它本身不产生BFC,但是它会产生匿名框(包含display: table-cell的框),而这个匿名框产生BFC。
更多请自行在网上搜索。

IFC(Inline Formatting Context)

IFC即行内框产生的格式上下文
IFC规则

在行内格式化上下文中
框一个接一个地水平排列,起点是包含块的顶部。
水平方向上的 margin,border 和 padding 在框之间得到保留
框在垂直方向上可以以不同的方式对齐:它们的顶部或底部对齐,或根据其中文字的基线对齐

行框
包含那些框的长方形区域,会形成一行,叫做行框
行框的宽度由它的包含块和其中的浮动元素决定,高度的确定由行高度计算规则决定
行框的规则:

如果几个行内框在水平方向无法放入一个行框内,它们可以分配在两个或多个垂直堆叠的行框中(即行内框的分割)
行框在堆叠时没有垂直方向上的分割且永不重叠
行框的高度总是足够容纳所包含的所有框。不过,它可能高于它包含的最高的框(例如,框对齐会引起基线对齐)
行框的左边接触到其包含块的左边,右边接触到其包含块的右边。

结合补充下IFC规则:

浮动元素可能会处于包含块边缘和行框边缘之间
尽管在相同的行内格式化上下文中的行框通常拥有相同的宽度(包含块的宽度),它们可能会因浮动元素缩短了可用宽度,而在宽度上发生变化
同一行内格式化上下文中的行框通常高度不一样(如,一行包含了一个高的图形,而其它行只包含文本)
当一行中行内框宽度的总和小于包含它们的行框的宽,它们在水平方向上的对齐,取决于 `text-align` 特性
空的行内框应该被忽略
即不包含文本,保留空白符,margin/padding/border非0的行内元素,
以及其他常规流中的内容(比如,图片,inline blocks 和 inline tables),
并且不是以换行结束的行框,
必须被当作零高度行框对待

总结:

  • 行内元素总是会应用IFC渲染规则
  • 行内元素会应用IFC规则渲染,譬如text-align可以用来居中等
  • 块框内部,对于文本这类的匿名元素,会产生匿名行框包围,而行框内部就应用IFC渲染规则
  • 行内框内部,对于那些行内元素,一样应用IFC渲染规则
  • 另外,inline-block,会在元素外层产生IFC(所以这个元素是可以通过text-align水平居中的),当然,它内部则按照BFC规则渲染

相比BFC规则来说,IFC可能更加抽象(因为没有那么条理清晰的规则和触发条件)
但总的来说,它就是行内元素自身如何显示以及在框内如何摆放的渲染规则,这样描述应该更容易理解。

其它

当然还有有一些其它内容:

  • 譬如常规流,浮动,绝对定位等区别
  • 譬如浮动元素不包含在常规流中
  • 譬如相对定位,绝对定位,Fixed定位等区别
  • 譬如z-index的分层显示机制等

这里不一一展开,更多请参考:https://bbs.csdn.net/topics/340204423

JS引擎解析过程

前面有提到遇到JS脚本时,会等到它的执行,实际上是需要引擎解析的,这里展开描述(介绍主干流程)。

JS的解释阶段

首先得明确: JS是解释型语言,所以它无需提前编译,而是由解释器实时运行
引擎对JS的处理过程可以简述如下:

  1. 读取代码,进行词法分析(Lexical analysis),然后将代码分解为词元token。
  2. 对词元token进行语法分析(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):JavaScript核心(晋级高手必读篇)
这里还有一篇文章可以帮助大家理解:作用域与作用域链
解释器解释完语法规则后,就开始执行,然后整个执行流程中大致包含以下概念:

  • 执行上下文,执行堆栈概念(如全局上下文,当前活动上下文)
  • VO(变量对象)和AO(活动对象)
  • 作用域链
  • this机制等

这些概念如果深入讲解的话内容过多,因此这里仅提及部分特性
执行上下文简单解释

  • JS有执行上下文
  • 浏览器首次载入脚本,它将创建全局执行上下文,并压入执行栈栈顶(不可被弹出)
  • 然后每进入其它作用域就创建对应的执行上下文并把它压入执行栈的顶部
  • 一旦对应的上下文执行完毕,就从栈顶弹出,并将上下文控制权交给当前的栈。
  • 这样依次执行(最终都会回到全局执行上下文)

譬如,如果程序执行完毕,被弹出执行栈,然后有没有被引用(没有形成闭包),那么这个函数中用到的内存就会被垃圾处理器自动回收:
image.png
然后执行上下文与VO,作用域链,this的关系是:
每一个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

image.png)image.png)image.png
image.png
VO与AO
VO是执行上下文的属性(抽象概念),但是只有全局上下文的变量对象允许通过VO的属性名称来间接访问(因为在全局上下文里,全局对象自身就是变量对象)
AO(activation object),当函数被调用者激活,AO就被创建了
可以理解为:

  • 在函数上下文中:VO === AO
  • 在全局上下文中:VO === this === global

总的来说,VO中会存放一些变量信息(如声明的变量,函数,arguments参数等等)
作用域链
它是执行上下文中的一个属性,原理和原型链很相似,作用很重要。
譬如流程简述:

在函数上下文中,查找一个变量foo
如果函数的VO中找到了,就直接使用
否则去它的父级作用域链中(__parent__)找
如果父级中没找到,继续往上找
直到全局上下文中也没找到就报错

image.png

this指针
这也是JS的核心知识之一,由于内容过多,这里就不展开,仅提及部分
注意:this是执行上下文环境的一个属性,而不是某个变量对象的属性
因此:

  • this是没有一个类似搜寻变量的过程
  • 当代码中使用了this,这个 this的值就直接从执行的上下文中获取了,而不会从作用域链中搜寻
  • this的值只取决中进入上下文时的情况

所以经典的例子:

var baz = 200;
var bar = {
    baz: 100,
    foo: function() {
        console.log(this.baz);
    }
};
var foo = bar.foo;
// 进入环境:global
foo(); // 200,严格模式中会报错,Cannot read property 'baz' of undefined
// 进入环境:global bar
bar.foo(); // 100

就要明白了上面this的介绍,上述例子很好理解
更多参考:
深入理解JavaScript系列(13):This? Yes,this!

回收机制

JS有垃圾处理器,所以无需手动回收内存,而是由垃圾处理器自动处理。
一般来说,垃圾处理器有自己的回收策略。
譬如对于那些执行完毕的函数,如果没有外部引用(被引用的话会形成闭包),则会回收。(当然一般会把回收动作切割到不同的时间段执行,防止影响性能)
常用的两种垃圾回收规则是:

  • 标记清除
  • 引用计数

Javascript引擎基础GC方案是(simple GC):mark and sweep(标记清除),简单解释如下:

  1. 遍历所有可访问的对象。
  2. 回收已不可访问的对象。

譬如:(出自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虚拟机作者。)
更多可以参考:
V8 内存浅析

其他

可以提到跨域

譬如发出网络请求时,会用AJAX,如果接口跨域,就会遇到跨域问题
可以参考:
ajax跨域,这应该是最全的解决方案了

可以提到web安全

譬如浏览器在解析HTML时,有XSSAuditor,可以延伸到web安全相关领域
可以参考:
AJAX请求真的不安全么?谈谈Web安全与AJAX的关系。

更多

如可以提到viewport概念,讲讲物理像素,逻辑像素,CSS像素等概念
如熟悉Hybrid开发的话可以提及一下Hybrid相关内容以及优化

感谢

感谢撒网要见鱼的分享。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!