2020年大厂面试指南 - 浏览器篇

2,141 阅读18分钟

导读

本文是面试指南系列文章的第三篇。

前两篇系列文章如下
2020年大厂面试指南 - Vue篇
2020年大厂面试指南 - 网络篇

本篇主要梳理浏览器相关的知识,主要包括以下三个方面:

  1. 浏览器渲染,包括回流和重绘相关知识
  2. 浏览器的缓存机制,涉及缓存读取机制,强缓存和协商缓存
  3. 浏览器的同源策略及跨域相关
  4. 浏览器输入url到页面展示都发生了什么

浏览器渲染

1. 请简述浏览器的渲染过程?

浏览器渲染过程如下:

  1. 首先解析html生成DOM树,解析CSS生成CSSOM树
  2. 将DOM树和CSSOM树结合生成渲染树(Render Tree)

生成渲染树过程: 遍历DOM树的每个可见节点,对每个节点,在CSSOM树中找到对应规则并使用,根据每个可见节点及其对应的样式规则,组合生成渲染树。(不可见节点:1.不会渲染输出的节点,包括link,script等;2.通过css隐藏掉的节点,如display:node的节点,需要注意的是visibility和opacity隐藏的节点,还是会显示在渲染树上)

  1. layout 布局 根据生成的渲染树,计算它们在设备视口(viewport)内的确切位置和大小。
  2. Painting 绘制 通过构造Render Tree和layout,我们已经得知哪些是可见节点,以及可见节点的样式和具体的几何信息(位置、大小),将渲染树的每个节点都转换为屏幕上的实际像素。
  3. display 将像素发送给GPU,展示在页面上

2. 什么是回流(Reflow)和重绘(Repaint)

上面介绍浏览器渲染过程时,有布局和绘制两步,回流和重绘就是触发这两个步骤。

回流: 页面布局或者几何属性发生改变,需要重新计算每个节点在设备视图上的具体位置及大小的过程,称为回流。
重绘: 节点样式或几何信息发生改变但不会影响到布局,但需要转化重新将渲染树的节点转换为屏幕上的实际像素。这个过程称为重绘。

3. 哪些操作会触发回流和重绘?

只要页面的布局或几何信息发生变化会影响到布局的,就会触发回流。例如下面情况会触发回流:

  1. 元素的位置、尺寸发生变化
  2. 添加/删除可见的dom元素
  3. 浏览器窗口大小发生变化

注意: 回流一定会触发重绘,而重绘不一定会回流。
节点样式或几何信息改变发生改变但不会影响到布局的,只会发生重绘,如visibility, color、background-color等改变。

4. 如何减少回流和重绘

由于回流和重绘会带来很大的性能开销,开发中我们要尽量避免或减少回流和重绘的次数。

  1. 避免频繁操作DOM,可以通过创建documentFragment,完成所有所有DOM操作后,最后再把它添加到文档中。
  2. 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  3. 对具有复杂动画的元素使用绝对定位,使其脱离文档流,否则会引起父元素及后续元素频繁回流。
  4. 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。

由于回流都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化回流过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是在获取某些布局相关的数据时,为了能访问到当前最新的布局信息,会强制清空队列。这些属性/方法包括 offsetTop、offsetLeft、offsetWidth、offsetHeight scrollTop、scrollLeft、scrollWidth、scrollHeight clientTop、clientLeft、clientWidth、clientHeight getComputedStyle() getBoundingClientRect()

  1. 开启css3硬件加速 使用css3硬件加速,会把需要渲染的元素放到特定的复合层中渲染,可以让transform、opacity、filters这些动画不会引起回流重绘。

关于回流和重绘推荐文章: github.com/chenjigeng/…

浏览器缓存机制

1. 浏览器的缓存读取规则是什么?

浏览器的缓存按照缓存位置可以分为:

  1. Service Worker
  2. Memory Cache
  3. Disk Cache
  4. Push Cache

浏览器读取缓存时,按照上面优先级顺序依次查找缓存,如果以上都没有命中缓存,则会发起请求。

Service Worker

Service workers 是运行在浏览器主线程外的独立线程,与浏览器其他的缓存机制不同,它让开发者能够更自由的控制要缓存哪些文件,以及如何匹配缓存和读取缓存。
Service workers中的缓存是“长期存储”,关闭tab或关闭浏览器都不会被清除。只有手动调用 cache.delete(resource) 或者容量超过限制,才会被全部清除。

如果 Service Worker 中没有命中缓存,需要调用 fetch 函数获取数据,然后会根据后续的缓存优先级查找是否命中缓存。
此处需要注意一点,不管后来命中了Memory Cache,Disk Cache, 还是从网络请求中获取的数据,浏览器都会标记为 from ServiceWorker

Memory Cache

Memory cache 指内存中的缓存。 几乎所有的请求资源都可以缓存入 memory cache,我们通过预加载器(Preloader),预加载指令(<link rel=preload>)等获取的数据都是缓存到Memory cache中。 当一个页面有两个相同的请求,实际上也只会请求一次,避免了浪费。
由于在内存中读取数据要比硬盘快,所以memory cache相对于Dist Cache读取更高效。
Memory Cache 是“短期存储”,正常情况下,当浏览器的Tab关闭后,当前Tab的 Memory Cache 便会被清除,特殊情况下,如果一个页面缓存内容特别多,占用了很多的内存,也可能在关闭Tab前,就清除了缓存。
需要注意:Memory Cache会忽视max-age=0, no-cache等http 头配置。但是如果设置头部字段设置了 Cache-Control: no-store,这样 Memory cache 就不会存储相关资源了。

Dist Cache

Dist Cache 指硬盘上的缓存。是一个持久化的缓存。 Dist Cache允许相同的资源在跨会话、跨站点的情况下使用。 Disk cache 会严格根据 HTTP 头信息中的各类字段来判定哪些资源可以缓存,哪些资源不可以缓存,以及哪些资源已经过期,哪些还可用。
命中Dist Cache缓存后,浏览器会从硬盘中读取对应的资源。 我们所熟知的强缓存,协商缓存,都属于Dist Cahce。

相比于Service Worker,Dist Cache的缓存可以逐条删除,当浏览器需要空间去缓存新的数据或更重要的数据时会自动清除旧的缓存数据。

Push Cache

Push Cache是HTTP/2推送的资源存储的地方,如果HTTP/2会话关闭了,储存在其中的资源会自动清除。从不同的会话发起的请求不会命中Push Cache中的资源。

所有未被使用的资源在Push Cache会储存一段时间,如果有一个请求命中了Push Cache中的资源,这个资源就会从Push Cache中移除。

对http/2还不了解的同学,可用看我的上一篇面试指南文章《网络篇》

更多关于 Push Cache,可以看一下这篇文章 jakearchibald.com/2017/h2-pus…

2. 什么是强缓存和协商缓存?

强缓存指客户端发起请求后,会先访问缓存中是否存在,如果存在,就返回对应资源。 协商缓存是需要向服务器发起请求,由服务器来决定是否使用缓存的资源。

3. 强缓存通过什么请求头控制,有什么区别?

强缓存通过Expires和Cache-Control字段控制。

Expires: 过期时间,表示资源的具体过期时间,使用绝对时间。例如:Thu, 01 Dec 1994 16:00:00 GMT
存在问题:如果服务端时间和电脑本地时间不一致,会导致缓存更新策略不一致。

Cache-Control:相对的时间单位,指定从请求的时间开始,允许获取的响应数据缓存多长时间(单位s)。例如:max-age=60.

如果Expires和Cache-Control两者都存在,Cache-control 的优先级高于 Expires。

4. Cache-Control有哪些属性?

public:表示响应可以被客户端和代理服务器缓存
private: 表示响应只能被客户端缓存
max-age=30:表示缓存有效时间为30s,超过30s需要重新请求
s-maxage=30:覆盖max-age,作用同max-age,只在代理服务器生效
no-sotre:不会缓存响应
no-cache:客户端缓会存资源,是否使用缓存则需要经过协商缓存来验证决定。需要注意,no-cache并不是不缓存响应资源,而是缓存后,要通过协商缓存来确定是否使用缓存。
max-stale=30:能接受的最大过期时间为30s,在30s秒内,即使缓存已经过期,也是要缓存。
min-fresh=30:表示希望在30s内获取最新的响应。

5. 描述下协商缓存的具体过程?

协商缓存通过以下字段控制

If-Modified-since: Last-Modifed
If-None-Match: Etag

If-Modified-since: Last-Modifed

浏览器初次请求时,服务端返回Last-Modified(最后修改时间)字段,浏览器再次发起请求时,请求头通过字段If-Modified-Since携带服务端返回的Last-Modified字段的值发给服务端,服务端拿到If-Modified-Since的值后,与服务器上资源最后的修改时间进行对比,相等则返回304,不同则说明资源已经更新。

If-None-Match: Etag

浏览器初次发起请求,服务端返回Etag(根据当前文件内容生成的唯一标识码)字段,浏览器再次发起请求时,会在请求头通过If-None-Match携带服务器返回的Etag的值,服务器拿到If-None-Match的值和服务器中资源当前的Etag值对比,相同则返回304,证明缓存有效,否则说明服务器资源已更新。

Last-Modifed 和 Etag 有什么区别

  1. Last-Modified只能精确到秒,在秒级改变的情况下是无法更新的,如果文件在1秒内改变了多次,可能会导致客户端得到的资源不是最新的。
  2. 关于Etag,每次客户端发出请求,服务端都会根据资源重新生成一个Etag,影响性能。
  3. 两者同时存在时,服务器的Etag的优先级大于Last Modified。

cookie localStorage sessionStorage 有什么区别区别

  1. 生命周期
    cookie 保存在内存中,随浏览器关闭失效,如果设置设置过期时间,在到期时间后失效
    localStorage 理论上永久有效,除非主动删除
    sessionStorage 仅在当前会话下有效,关闭页面或浏览器后会被清除
  2. 存储容量 一个域下的cookie存储容量4k左右,超出将清除之前的设置。且有些浏览器会对一个域下的cookie个数做限制。
    localStorage: 每个源下的大小限制5MB左右,不同浏览器不同
    ssionStorage: 不容浏览器不同,有些浏览器不会限制,有些会限制5MB左右
  3. 是否与服务端交互 cookie 和 storage 都会保存在客户端,其中cookie会参与和服务端的交互,每次请求都会携带,而storage不会参与和服务端的交互。
  4. API易用性
    cookie可以通过document获取全部cookie,获取的cookie是字符串格式的,操作起来比较麻烦,需要自己封装方法,或使用第三方库js-cookie等完成。 storage提供了比较完善友好的API,setItem,getItem,clear,removeItem等
  5. 浏览器兼容性
    cookie没有兼容性问题,storage在IE8+浏览器可以正常使用

同源策略

1. 什么是浏览器的同源策略?

同源策略及限制: 为了安全,浏览器会限制不同源(协议,域名,端口号都相同为同源)的文件之间的交互, 主要限制:

  1. cookie,localstorage,indexDB无法读取
  2. DOM无法获取
  3. AJAX请求不能正常发送

2. 如何进行跨域?

解决跨域的方法有很多,这里不展开说。可以借助:jsonp,Cors,iframe,window.name,postMessage等实现跨域。
详细的原理推荐一篇文章:正确面对跨域,别慌,感兴趣的同学也可以读一下这本书《第三方JavaScript编程》,另外推荐一个开源的库,专门用来解决跨域问题,可以兼容低端浏览器--easyXDM。

3. 请简述JSONP的原理

jsonp实现跨域,主要是通过script标签,因为script标签对请求资源没有跨域限制。
前后端约定一个字段名,比如callback,用来传递回调函数名,后端处理得到数据后,拼出“将数据传递给该函数并执行该函数”的js语句,将其返回给浏览器,浏览器解析执行,从而使得前端可以使用对应的callback函数,拿到数据。
因为jsonp是利用script标签来实现,所以jsonp只能支持GET请求,这是它的一个局限。

let script = document.createElement('script');
script.src = 'http://www.yushihu.com?callback=myCallback';
document.body.appendChild(script);
function myCallback(data) {
  console.log(data)
}

4. 请简述一下CORS的原理

CORS(跨域资源共享),实现的基本思想是通过自定义的HTTP头部让浏览器与服务器之间进行沟通,从而决定请求或响应应该是成功,还是失败。
对于开发者来说,CORS通信与正常的的ajsx没有差别,代码也完全一样。只要浏览器发现ajax请求跨源,就会自动在头部添加一些附加的头信息,用户也不会感知。
所以CORS通信实现的关键是服务器,只要服务器支持了CORS接口,就能够进行跨域请求。

CORS请求有两类:

只要满足以下两个条件,浏览器就会认为是简单请求,否则就会认为是非简单请求。

  1. 请求方法是GET、HEAD、POST中的一个。
  2. HTTP的头信息不超出以下几种字段,Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type:只能是三个值中的一个application/x-www-form-urlencoded、multipart/form-data、text/plain。

简单请求

对于简单请求,浏览器会在头信息之中,增加一个Origin字段,Origin字段用来说明,本次请求来自哪个源,服务端会通过这个字段来判断是否允许这次请求。
如果该源在服务端允许的范围内,服务端会在响应的头部信息中增加以下字段:

  1. Access-Control-Allow-Origin 必须字段,表示服务器接受的域名
  2. Access-Control-Allow-Credentials 可选字段,表示是否允许发送Cookie
  3. Access-Control-Expose-Headers 可选字段,表示CORS请求增加了的字段。

在CORS请求中,通过XMLHttpRequest对象的getResponseHeader()只能拿到6个字段(Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma),想获取其他字段就必须在Access-Control-Expose-Headers里面指定。

如果该源不在浏览器的允许范围内,浏览器会正常响应请求,但不会增加上述字段,浏览器通过判断头信息中没有Access-Control-Allow-Origin字段,就知道请求出错了,并抛出错误信息。

非简单请求

对于非简单请求,浏览器会在正式发起请求前,发送一个“预检”请求,向服务器询问当前域名是否在许可的名单之中以及可以使用哪些头部信息,如果得到服务器的肯定答复,则会发出正式的请求,否则会报错。
"预检"请求的请求方法是OPTIONS,会携带字段:

  1. Origin 发起请求的源
  2. Access-Control-Request-Method 发起请求使用的Method
  3. Access-Control-Request-Headers 逗号分隔,指定请求会额外发送的头信息字段。

服务器收到"预检"请求以后,会检查Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段,如果通过则在响应的头信息中会增加以下字段:

  1. Access-Control-Allow-Origin 与简单请求该字段一致
  2. Access-Control-Allow-Methods 必须字段,表示服务器支持的所有跨域请求的方法
  3. Access-Control-Allow-Headers 表示服务器支持的所有头部字段, 如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。
  4. Access-Control-Allow-Credentials 与简单请求该字段一致
  5. Access-Control-Max-Age 可选,表示本次预检请求的有效期

预检请求通过,后续的CORS通信就和简单请求一致了。
如果预检请求,服务端做出的回应是不允许,此时服务端也会正常响应,只是响应头信息中不会携带和CORS有关的头部字段,浏览器据此判断请求失败,抛出错误信息。

页面请求及渲染过程

1.浏览器输入url到页面展示都发生了什么

浏览器输入url后主要发生以下过程:

  1. DNS解析,将域名解析成url
  2. 建立TCP连接,进行3次握手
  3. 发起http请求,服务端处理请求,返回相应的资源
  4. 浏览器解析资源,进行页面渲染
    面试中最好详细讲解下每一步发生的事情,来表现自己对细节的掌握程度

DNS解析

具体步骤:
a. 查询本地host文件,如果本地hosts文件有这个域名和ip的映射关系, 直接返回对应ip b. 如果hosts文中中没有对应域名的映射,则查找本地DNS解析器缓存
c. 如果host文件和本地DNS解析器缓存中都么有域名的映射,则会首先到本地DNS服务器进行查询
d. 如果本地DNS服务器已经缓存了此域名的映射,则直接返回
e. 如果本地服务器的缓存中没有对应映射关系,则本地DNS服务器会对该域名进行解析,如果要查询的域名是由当前服务器解析,则将解析结果返回。
f. 如果要查询的域名不是由当前的DNS服务器进行解析则会向根域的DNS服务器发起请求,根域DNS服务器会根据域名返回一个负责该顶级域名的DNS服务器ip,本地服务器拿到ip后向这个顶级域名DNS服务器请求 g. 如果这个顶级DNS服务器无法解析这个域名,会返回二级DNS服务器的ip,这样一直重复,知道找到对应的ip映射。整个DNS查询的过程,只要某个DNS服务器上有对应的缓存,就会直接返回,不会继续查找。
更多关于DNS解析的过程,可以参考我之前的文章 揭开IP地址的面纱

建立网络连接

这里会先建立TCP连接,可以参考系列文章的上一篇章2020年大厂面试指南-网络篇中三次握手的介绍。
如果是https请求,这里还会再进行SSL的握手过程,可以参看2020年大厂面试指南-网络篇中关于https握手的部分。
由于这里再上篇文章中已经写过,这里只给出文章地址,但是面试中一定要尽可能详细的给出答案,关于这个面试题,回答的越详细越好。

发起http请求

建立连接后,开始发起http请求,此时会按照本文上面浏览器缓存读取机制,去依次读取缓存,如果命中缓存,直接使用缓存中资源。如果没有命中缓存,则向服务端发起http请求,服务端接收请求,处理后返回相应的资源。

浏览器解析资源

浏览器接收到资源后,会对资源进行解析,这里就到了本文上面的渲染部分,这里不再赘述。
这里需要补充的是,解析过程中,如果遇到外部资源,会去加载对应资源,对于js资源来说,如果是同步资源,会加载资源,并执行其资源内的代码,阻塞渲染流程,异步加载的js资源,又分为设置defer和async两种属性。

其中defer要等到整个页面在内存中正常渲染结束(DOM结构完全生成,以及其他脚本执行完成),才会执行。 async是“下载完就执行”。 如果有多个defer脚本,会按照他们在页面上出现的顺序加载,而async脚本不能保证加载的顺序。

参考文章

一文读懂前端缓存
深入理解浏览器的缓存机制
跨域资源共享 CORS 详解

欢迎扫码进入微信交流群,另外系列文章会在公众号「前端小苑」同步发出,欢迎关注。