title: 2.Chrome
order: 2
浏览器基础
参考:
- 阮一峰-浏览器同源政策及其规避方法
- 阮一峰-跨域资源共享 CORS 详解
- 阮一峰-Cookie 的 SameSite 属性
- Jian blog-浏览器进程和线程
- Jian blog-前端网络安全
- 高频前端面试题汇总之浏览器原理篇
- 从 8 道面试题看浏览器渲染过程与性能优化
01.浏览器渲染引擎
- 一个渲染引擎包括:HTML解析器,CSS解析器,JS引擎,布局layout模块,绘图模块.
- 解析:解析代码 、 渲染:渲染页面
- js线程:执行js代码。与GUI线程互斥,当一个执行时,另一个会被强制挂起。当js执行一个时间负责度高的算法时,导致GUI渲染线程被挂起太久,会导致页面卡顿,解决办法可以通过web worker解决。
02.大致渲染过程
-
遇到HTML标记,解析HTML,
生成Dom树
(就是一块内存) -
遇到style/link,解析构建出
CSS样式树
- style标签中的样式由html解析器进行解析,容易
闪屏
,样式太多,内容先出来了 - link样式:同步解析、会等样式加载完毕,浏览器才渲染页面、但是
阻塞
浏览器渲染,时间会长一点,实际开发用loading,用户体验好(避免闪屏)、阻塞其后面的js语句的执行
- style标签中的样式由html解析器进行解析,容易
-
遇到script,处理script标记、绑定事件、
修改DOM树/CSS树
等(要看有无defer或者async) -
将DOM树与CSS树
合并
成一个渲染树
-
根据渲染树来
渲染
,以计算每个节点的几何信息(这一过程需要依赖GPU) -
最终各个节点
paint
绘制到屏幕上—这其中细节很多:计算样式、计算布局树、分层、绘制、分块\光栅化、合成、显示
-
以上这些模块依赖很多其他基础模块,包括如:网络、图片解码器等
03.JS阻塞
- 暂停文档的解析,将控制权移交给 JavaScript 引擎
- 阻塞后续DOM解析,阻塞页面渲染,阻塞后续js执行
- 无论css阻塞,还是js阻塞,都不会阻塞浏览器加载外部资源(图片、视频、样式、脚本等)
- 浏览器自带预解析,但不会影响DOM解析
04.DOMContentLoaded和onload事件
//js写在body(DOM解析)前面的时候,可以先加载资源,等DOM解析好了后再调用
window.onload=function(){}//DOM树构建完并且网页所依赖的资源都加载完之后
window.addEventListener('DOMContentLoaded',function(){})//在DOM树(解析)完成之后
05.Cookie
每个域名下Cookie的数量不能超过20个,每个Cookie的大小不能超过4kb
一级域名相同,二级域名不同,依然可以突破同源策略,共享Cookie
document.domain = 'example.com';
注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法,规避同源政策,而要使用下文介绍的PostMessage API。
另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如.example.com
。
Set-Cookie: key=value; domain=.example.com; path=/
这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。百度就是这样做的
组成字段:name,value,size,path,secure,domain,http,expires/max-size
06.解决跨域
-
JSONP
-
WebSocket因为有Origin字段,可以判断白名单,是否运行通信
-
CORS:Access-Control-Allow-Origin: *
一旦服务器通过了"预检options"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个
Origin
头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin
头信息字段。 -
postMessage 跨域
-
nginx代理跨域:通过配置文件设置请求响应头Access-Control-Allow-Origin…等字段,与nodejs 中间件代理跨域原理一致
-
document.domain + iframe跨域,此方案仅限主域相同,子域不同的跨域应用场景;location.hash + iframe跨域;window.name + iframe跨域
07.defer和async
- script标签中的defer属性,延迟执行脚本,解析完
</html>
后执行,执行顺序不变,即第一个defer脚本会在第二个defer脚本前执行 - script标签中的async属性,异步执行脚本,文件加载完成后执行,无法保证执行顺序,哪个加载完哪个执行。
08.link、@import、内联样式
其中link和@import都是导入外部样式。它们之间的区别:
- link:浏览器会派发一个新等线程(HTTP线程)去加载资源文件,与此同时GUI渲染线程会继续向下渲染代码
- @import:GUI渲染线程会暂时停止渲染,去服务器加载资源文件,资源文件没有返回之前不会继续渲染(阻碍浏览器渲染)
- style:GUI直接渲染
外部样式如果长时间没有加载完毕,浏览器为了用户体验,会使用浏览器会默认样式,确保首次渲染的速度。所以CSS一般写在headr中,让浏览器尽快发送请求去获取css样式。
所以,在开发过程中,导入外部样式使用link,而不用@import。如果css少,尽可能采用内嵌样式,直接写在style标签中。
09.浏览器渲染进程的线程有哪些?
GUI渲染线程 JS引擎线程 时间触发线程 定时器触发进程 异步http请求线程
10. 对Service Worker的理解
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。
Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install
事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据
11.浏览器缓存更多理解
强缓存策略可以通过两种方式来设置,分别是 http 头信息中的 Expires 属性和 Cache-Control 属性。
no-cache和no-store的区别
- no-cache 是指先要和服务器确认是否有资源更新,在进行判断。也就是说没有强缓存,但是会有协商缓存;
- no-store 是指不使用任何缓存,每次请求都直接从服务器获取资源。
协商缓存也可以通过两种方式来设置,分别是 http 头信息中的Etag 和Last-Modified属性。
当 Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器上 Etag 的值都不一样,因此在考虑负载平衡时,最好不要设置 Etag 属性。
12.减少回流与重绘
- 操作DOM时,尽量在低层级的DOM节点进行操作
- 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
- 避免频繁操作DOM,可以创建一个文档片段
documentFragment
,在它上面应用所有DOM操作,最后再把它添加到文档中 - 将元素先设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
- 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制。
13.正向代理和反向代理的区别
反向代理的结构是一样的,都是 client-proxy-server 的结构,它们主要的区别就在于中间这个 proxy 是哪一方设置的。在正向代理中,proxy 是 client 设置的,用来隐藏 client;而在反向代理中,proxy 是 server 设置的,用来隐藏 server。
14.事件委托
如何阻止事件冒泡
- 普通浏览器使用:event.stopPropagation();return false两个都阻止
- IE浏览器使用:event.cancelBubble = true;
阻止默认事件:event.preventDefault()
事件委托的理解:利用事件冒泡机制,减少内存消耗,能拿到当前this
15.事件循环Event Loop的理解
因为 js 是单线程运行的,在代码执行时,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码时,如果遇到异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当异步事件执行完毕后,再将异步事件对应的回调加入到一个任务队列中等待执行。任务队列可以分为宏任务队列和微任务队列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务队列中的任务都执行完成后再去执行宏任务队列中的任务。
Event Loop 执行顺序如下所示:
- 首先执行同步代码,这属于宏任务
- 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
- 执行所有微任务
- 当执行完所有微任务后,如有必要会渲染页面
- 然后开始下一轮 Event Loop,执行宏任务中的异步代码
16.Node 中的 Event Loop 和浏览器中的有什么区别?process.nextTick 执行顺序?
Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
(1)Timers(计时器阶段):初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Pending callbacks 阶段。
(2)Pending callbacks:执行推迟到下一个循环迭代的I / O回调(系统调用相关的回调)。
(3)Idle/Prepare:仅供内部使用。
(4)Poll(轮询阶段):
- 当回调队列不为空时:会执行回调,若回调中触发了相应的微任务,这里的微任务执行时机和其他地方有所不同,不会等到所有回调执行完毕后才执行,而是针对每一个回调执行完毕后,就执行相应微任务。执行完所有的回调后,变为下面的情况。
- 当回调队列为空时(没有回调或所有回调执行完毕):但如果存在有计时器(setTimeout、setInterval和setImmediate)没有执行,会结束轮询阶段,进入 Check 阶段。否则会阻塞并等待任何正在执行的I/O操作完成,并马上执行相应的回调,直到所有回调执行完毕。
(5)Check(查询阶段):会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段。
(6)Close callbacks:执行一些关闭回调,比如socket.on(‘close’, …)等。
17.V8的垃圾回收机制是怎样的
V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。
(1)新生代算法
新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。
在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。
(2)老生代算法
老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。
先来说下什么情况下对象会出现在老生代空间中:
- 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
- To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。
在老生代中,以下情况会先启动标记清除算法:
- 某一个空间没有分块的时候
- 空间中被对象超过一定限制
- 空间不能保证新生代中的对象移动到老生代中
在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行。
清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存。
18.哪些操作会造成内存泄漏?
第一种情况是由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
第二种情况是设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
第三种情况是获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。
第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。
19.XSS和CSRF
csrf跨站请求伪造
:解决:设置token
、检测refer
、Cross-site request forgery、设法伪造带有正确 Cookie 的 HTTP 请求
xss跨域脚本攻击
:解决:字符过滤(编码)、httponly
字段、SameSite字段、Cross Site Scripting、有存储型反射型DOM型