- 2024前端年末备战面试题——HTML篇
- 2024前端年末备战面试题——CSS篇
- 2024前端年末备战面试题——网络篇
- 2024前端年末备战面试题——JavaScript篇
- 2024前端年末备战面试题——框架篇(Vue)
- 2024前端年末备战面试题——构建工具与git
浏览器篇
本文是本人根据MDN上的解答或者网上一些其他朋友的文章以及自己的理解,整理归纳出来的一篇浏览器方面的面试题,其中不免有许多漏掉的问题或是答案,发现错误的或者是有什么问题需要补充的朋友们,可以在评论区留言,大家虚心交流,一起进步。
1. 进程和线程
进程是cpu
资源分配
的最小
单位; 线程是cpu调度
的最小
单位;
进程和线程的关系
- 通俗来讲,每一个
执行中的程序
就可以称之为进程
,而一个进程由多个线程
组成; 一个进程
中任意一个线程
出错,就会导致整个进程出错;一个进程
中的所有线程
共享进程中的数据;不同
进程之间的内容相互隔离
;- 关闭进程时,进程中的
所有线程
也会被关闭;
浏览器的进程和线程
现在许多浏览器采用的是多进程、多线程
的架构模式,比如chrome
,就是一个多进程
并且多线程
架构的浏览器。
多线程
架构的浏览器,打开的每个Tab
是一个线程
,而多进程
的浏览器,打开的每一个Tab,都是一个进程
。
多进程浏览器的优势
- 我们打开的每个tab页,即使卡顿、崩溃,也只是那一个进程会受到影响,
其他进程不会受到影响
; - 现在很多cpu都是多核的,而多进程可以完美的发挥
cpu的优势
; - 浏览器中还有插件进程,同时也保证了不同插件发生崩溃时
不会影响到浏览器的使用
;
2. 浏览器内核
浏览器内核是属于多进程
浏览器的其中一个进程
,也被称为渲染进程
,它拥有多个线程
。
(1)主流浏览器的内核
chrome
使用blink
内核,之前使用的是由Chromium内核(fork自webkit)
;safari
使用的是webkit
内核,是webkit的鼻祖
;firefox
使用的是Gecko
内核;IE/Edge
使用的是Trident
内核;Opera
使用的是blink
内核(因为是和谷歌一起研发的);
(2)浏览器内核中的线程
渲染进程
中一般由以下几种线程
组成:
- GUI渲染线程
- JavaScript引擎线程
- 定时器触发线程
- 事件触发线程
- 异步http请求线程
3. JS是单线程还是多线程
js是单线程的
,原因是因为js最初的使用场景最多的就是与用户的交互
,这就离不开dom的操作
,只有单线程,才能保证同一时间
,用户只能操作一种功能
,如果是多线程,就会造成很多问题。
4. 既然js是单线程,那Web Worker是什么?
Web Worker 使得在一个独立于 Web 应用程序主执行线程的后台线程中运行脚本操作成为可能。这样做的好处是可以在独立线程中执行费时的处理任务,使主线程(通常是 UI 线程)的运行不会被阻塞/放慢。
Web Worker
是浏览器为JavaScript
创建的一个多线程环境
,但它并不
代表js是多线程的
。
在Web Worker
中是不能
操作dom的,这是因为我们通过new Worker()
创建一个Worker时,虽然浏览器单独开辟出来了一个新的线程,但是当前线程只是帮助我们运行一些复杂
、耗时
的任务,它还是受主线程控制
的,因此,严格意义上来说,js是单线程
这一点,从未改变过。
Web Worker的限制
同源限制
:worker
中运行的脚本文件,必须和主线程的脚本文件同源
;文件限制
:worker
不能加载本地文件
,必须是网络文件
;不能操作DOM
:不能直接操作dom
对象,只能通过和主线程传递消息,由主线程操作dom
;语法限制
:某些函数
或者类
不能在worker中使用;可以使用的参考这个通信限制
:不能和主线程直接通信
,需要使用postMessage
方法进行通信;
5. 浏览器缓存机制
(1)何时触发缓存
- 发起请求时,浏览器首先会在缓存中查找
请求结果
以及缓存标识
; - 接收响应时,浏览器会将请求数据中的的
缓存标识
和请求结果
进行缓存;
(2)缓存策略
缓存策略分为强缓存
和协商缓存
;
(3)缓存的位置
Service Worker
;Memory Cache
:内存中的缓存;Disk Cache
:磁盘中的缓存;Push Cache
:(推送缓存)是HTTP/2
中的内容;
浏览器会先查看Service Worker,然后去内存缓存查找缓存,然后去磁盘缓存,以此类推...
(4)强缓存
强缓存
有两种标识,一种是HTTP1.0时代
的expires
,一种是HTTP1.1时代
的Cache-Control
。
expires
expires
的值是服务端的时间
,它是一个具体的时间
,我们判断是否命中强缓存
时,是拿本机时间和该时间做对比判断的,所以说如果我们修改了本机时间
, 可能会造成强缓存失效
。
cache-control
cache-control
有多个值,一般我们用其中的max-age
属性来判断是否命中强缓存
,它的值是一个具体的秒数
,如果响应头既有expires又有cache-control
,那么cache-control的优先级更高
。
cache-control常用的属性 | cache-control常用的属性的含义 |
---|---|
max-age | 单位为秒,代表缓存过期时间,是相对于上次请求成功的时间对比 |
s-maxage | 与 max-age 不同之处在于,其只适用于公共缓存服务器 ,比如资源从源服务器发出后又被中间的代理服务器接收并缓存。当使用 s-maxage 指令后,公共缓存服务器将直接忽略 Expires 和 max-age 指令的值。 |
public | 代表当前请求结果可以被客户端 或者代理服务器 进行缓存 |
private | 代表当前请求结果只可以 被客户端 进行缓存,代理服务器不可以 缓存,设置了该值,s-maxage就会被忽略 |
no-cache | 在响应头和请求头中出现的含义不同 ,在请求头 出现时意为不管缓存有没有过期,必须向服务器验证资源有效性,说白了就是强制使用协商缓存 ,在响应头 中出现时,意为每次缓存之前,需要向服务器验证响应头是否被篡改 |
no-store | 代表不进行任何缓存 |
must-revalidate | 一旦缓存过期,必须 向服务器/代理服务器验证资源的有效性 |
所有属性如下:
(5)协商缓存
协商缓存
有两个版本的请求头和响应头,在HTTP1.0
时,响应头为Last-Modified
,请求头为Last-Modified-Since
,在HTTP1.1
时,响应头为ETag
,请求头为If-None-Match
。
二者的区别:
- ETag的
精确度
要高于
Last-Modified,因为ETag的值是根据文件内容计算出来的哈希值
,而Last-Modified仅仅是一个时间,特殊的一些情况下,通过Last-Modified去判断并不准确,比如它的单位是秒
,如果1s内修改多次
,就会造成数据不一致的情况,又或者是仅仅编辑了文件,并未真正修改,但此时Last-Modified的值会变,这时候明明可以使用协商缓存,也不会使用了; - 性能上Last-Modified要好,也是因为它只是一个时间,并不需要根据文件内容去计算;
(6)发起请求时缓存的详细过程
-
浏览器第一次加载资源,缓存中没有任何内容,直接正常发起请求,服务器
除了返回资源文件以外
,还会给response header
添加该资源的ETag
(如果不支持http1.1
,则是最后一次修改的时间Last-Modified
),浏览器把资源文件和response header
及该请求的返回时间一并缓存; -
下一次加载这个资源时,浏览器会先查看请求这个资源的
响应头
,如果发现这个请求支持强缓存
,那么就通过比较这次发起请求的时间和上一次返回数据成功
的时间差,如果没有超过cache-control
设置的max-age
,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件 (如果浏览器不支持HTTP1.1
,则用expires
判断是否过期);如果未命中强缓存
,则看这个请求是否支持协商缓存
,如果支持,就在请求头
添加If-None—Match
(值为响应头中的ETag
) (如果不支持http1.1
的话,就添加If-Modified-Since
,值为响应头的Last-Modified
),然后向服务器发送请求; -
服务器收到请求后,根据
ETag
的值判断被请求的文件有没有做修改,ETag
值一致则没有修改,命中协商缓存,返回304
;如果不一致则有改动,直接返回新的资源文件
带上新的ETag
并返回;(如果不支持ETag,就会使用请求头的If-Modified-Since
和该文件最后一次在服务器修改的时间
进行对比,如果一致,则命中协商缓存,不一致就返回新的数据和Last-Modified。)
6. Service Worker
Service worker 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。
- Service Worker
基于
Web Worker,只是比Web Worker多了离线缓存
的功能; - 出于
安全
考量,Service worker只能由 HTTPS 承载
;
Service Worker生命周期
-
注册(register)
:如果注册成功,那么Service Worker将会被下载到客户端; -
安装(install)
:安装分为两种情况,(1)这是
首次启用
Service Worker:- 首次启用就会直接尝试安装,安装完成后就会直接进入下个生命周期
激活
;
(2)之前
已经存在激活
的Service Worker:- 如果现有 service worker 已启用,新版本会在后台安装,但仍不会被激活——这个时序称为
worker in waiting
; - 直到所有已加载的页面
不再使用
旧的 Service Worker(使用旧的Service Worker的页面全部被关闭掉),才会激活新的
Service Worker; - 可以调用
skipWaiting()
方法,直接激活新的Service Worker,后续所有页面都将被新的Service Worker
接管;
- 首次启用就会直接尝试安装,安装完成后就会直接进入下个生命周期
-
激活(activate)
:service worker 将立即控制页面,但是只会控制那些在register()
成功后打开的页面。
访问Service Worker页面的缓存数据
- 浏览器打开一个页面;
- 为当前页面打开一个新的进程;
- 主线程执行当前页面的任务;
- 如果
没有碰到
Service Worker,则不进行
任何处理; - 如果
碰到了
Service Worker,则开启一个Service Worker
线程,并记录下当前页面的URL; - 下次再访问这个URL,会自动
打开Service Worker
线程,然后访问缓存数据
;
7. 启发式缓存
如果响应头没有
任何强缓存相关
的属性,但是有Last—Modified
属性,那么浏览器默认会采用一个启发式缓存。
该缓存通过公式计算出一个时间,在这个时间内,使用强缓存
,公式为Last-Modified Time - Date * 0.1 (10%)
。
8. 浏览器渲染流程
- 从上到下
解析HTML
,生成DOM树
;(如果遇到了不带async
或者defer
的js脚本,会阻塞
HTML的解析,如果是带async
的js脚本,在加载js脚本时不会阻塞,但是在执行时会阻塞); - 解析
CSS资源
,生成CSSOM树
,CSS的解析不会阻塞
HTML的解析,因为CSS的解析是在预解析线程
执行,而HTML的解析是在主线程
执行,但是会阻塞
HTML的渲染; - 将解析好的CSS合并到
主线程
,将CSSOM树和DOM树合并,构建渲染树(Render); - 依次计算DOM树中每个节点的样式,得到最终样式;
- 布局(Layout),将DOM树中的每个节点进行布局,得到布局树;
- 进行分层,浏览器会根据布局树,对整个页面进行分层,后续哪些节点有改动,只需要改动某一层的信息,层叠上下文或者CSS属性的
will-change
都会影响浏览器的分层; - 进行绘制,浏览器发出
绘制指令
,交给分层之后的每一层进行绘制,至此,主线程工作结束,剩下的交由合成线程
进行处理; - 分块,合成线程从线程池中调度
更多的线程
对每个图层
进行分块,将他们分成更小的区域
; - 光栅化,分好块之后,合成现场将分好的块交由
GPU线程
,GPU会以极快的速度完成光栅化,为了使页面更快的展现出内容,会优先处理靠近视口位置的块; - 画,完成光栅化之后,每个块就变成了一块一块的
位图
,然后交由合成线程,合成线程将每一块位图画到屏幕相应的位置,并查看是否有缩放
、旋转
等操作,而transform
就是在这一步完成的,这也是为什么transform
性能要更好的原因。最终由浏览器主线程完成最终的展示。
9. document.readystate、DOMContentLoaded、window.onload
(1)document.readystate
document.readystate
代表了文档的加载状态,它的值可以是以下三种之一:
loading(正在加载)
interactive(可交互)
complete(完成)
(2)DOMContentLoaded
-
相当于document.readystate的
interactive
; -
当 HTML 文档完全解析,且所有延迟脚本下载和执行完毕后,会触发
DOMContentLoaded
事件; -
它不会等待图片、子框架和异步脚本等其他内容完成加载;
(3) window.onload
load
事件在整个页面及所有依赖资源如样式表和图片都已完成加载时触发。它与DOMContentLoaded
不同,后者只要页面 DOM 加载完成就触发,无需等待依赖资源的加载。- 相当于document.readystate的
complete
;
总结
- 浏览器开始
解析HTML
,此时document.readystate
为loading
; - 解析中遇到不带
async
和defer
的script脚本
时,需要等待script脚本
下载完成并执行后,才会继续解析HTML
; - 遇到带
defer
的script脚本
,等待它们下载完毕,这些脚本会在HTML解析之后
开始执行,等待它们执行完毕; - 当 HTML 文档完全解析,
且
所有延迟脚本下载和执行完毕
,document.readyState
变成interactive
,触发DOMContentLoaded事件
; - 此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成载入和执行(如
<script async src=xxx >
),document.readyState
变为complete
,window
触发load
事件;
10. 说一下js文件、css文件、html、图片资源之间的阻塞关系
CSS的解析
和HTML的解析
没有阻塞关系,因为二者在不同的线程完成
,CSS的解析
在预解析线程
,HTML的解析
在主线程
;CSS的解析
会阻塞
HTML的渲染
,因为HTML的渲染需要CSS样式,如果CSS没有解析完毕,HTML无法完成渲染;- 不带
async
或者defer
的script标签
会阻塞HTML的解析
,并且会等待执行完毕之后
才会继续解析HTML; - 带
async
的script标签
在下载时不会影响HTML的解析
,因为是在不同的线程进行下载,但是下载完毕之后会立即执行
,执行会阻塞
HTML的解析; - 带
defer
的script标签
,不会影响HTML的解析
,因为它在不同的线程进行下载,并且它会等待HTML解析完毕
再进行执行; CSS的解析
和js的下载
不会有任何阻塞关系,因为它们在不同的线程,但是会影响js的执行
,因为js可能会操作dom修改css
,所以说必须等待css加载完毕,才能继续执行js;图片资源
的加载,不会和任何产生冲突,因为它会在其它线程进行异步下载;
11. CSS为什么最好要放在头部
首先CSS不会阻塞HTML的解析
,所以放头部和尾部没太大影响,但是CSS会阻塞HTML的渲染
,如果把CSS放在尾部,HTML解析完毕了,过了一会CSS又解析完毕了,浏览器又要根据CSSOM树重新计算HTML节点的样式,重新进行布局,可能会造成回流等现象。
12. js文件为什么放到尾部比较好
因为js文件会阻塞
HTML的解析,如果放在头部,js文件过大时,会导致HTML解析长时间无法完成
,DOM树无法构建,页面无法渲染,造成长时间白屏
的现象,影响用户体验。
13. 浏览器跨域
产生跨域的原因
之所以产生跨域
,是因为浏览器的同源策略
,即浏览器规定必须协议
、ip地址
、端口号
保持一致时,才算同源。
同源策略限制了哪些行为
不能
发送网络请求;不能
获取或操作dom;不能
获取本地存储,如cookie
、storage
、IndexedDB
等;
如何解决跨域
跨域资源共享
(CORS)jsonp
nginx反向代理
postMessage跨域
设置代理服务器
CORS解决跨域
跨源资源共享(CORS,或通俗地译为跨域资源共享)是一个基于 HTTP 头的机制,该机制通过允许服务器标记除了它自己之外的其他来源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标明有HTTP器方法而真实请求中会用到的头。
- 当我们跨域进行发起请求时,如果是
简单请求
,浏览器就不会进行预检(option)请求
,如果是一个复杂请求
,就会先发出预检请求
; 预检请求
会根据服务端的响应头
中的Access-Control-Allow-origin
来判断是否支持跨域,如果支持则正常发送请求,不支持则在控制台报错;- 这种方法主要由
服务端
进行配置,前端无需关注太多;
jsonp进行跨域
jsonp的原理就是利用了script标签不受浏览器同源策略的限制
,然后和后端一起配合来解决跨域问题的。
具体的实现方式如下:
- 在客户端创建一个
script标签
, - 把需要请求的接口地址
拼接
一个回调函数名称
作为参数传给服务端,作为script标签的src属性
,然后把script标签添加到body中; - 当服务端接收到客户端的请求时,会
解析得到回调函数名称
,然后把数据
和回调函数名称
拼接成函数调用的形式返回给客户端; - 客户端拿到该数据之后,进行解析,然后
调用该回调函数
,在回调函数中就可以拿到服务端返回的数据
;
nginx反向代理
通过配置nginx文件
,把客户端发起的请求代理到真实的服务器地址
,然后将服务器返回的数据交给客户端;
postMessage跨域
window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议,端口号,以及主机设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。
- 在两个窗口之间,通过
window.postMessage()
发送消息和数据; - 通过
window.onmessage
来接收postMessage
发出的消息和数据; - 可以利用该功能使用
window.open()
打开窗口,然后通过postMessage
实现浏览器跨页签
通信;
代理服务器跨域
主要是利用了服务器请求服务器不受浏览器同源策略的限制
实现的,但是本地和代理服务器之前也会存在跨域问题
,只是说如果我们自己有能力搭建代理服务器,我们就可以自己搭建代理服务器,然后实现本地和代理服务器之间的跨域请求,再通过代理服务器和真实服务器之间的请求拿到最终数据,不必再麻烦自己的服务端同事(求人不如求己)。
14. 浏览器存储数据方案
浏览器常见的存储数据
的方案一共有四种,分别是Cookie
、LocalStorage
、SessionStorage
、IndexedDB
Cookie
Cookie
主要被用于用户身份的验证
,以及用户数据的持久性
,主要是为了使无状态的
HTTP在某些特殊场景变的有状态
,主要体现在我们发送网络请求时,Cookie会被请求头携带
,并可以被服务端进行接收
,服务端就可以通过Cookie中存储的一些信息进行验证。
- Cookie的存储大小只有4kb,这是每个HTTP请求都会携带Cookie,所以当我们的存储量过大时会使HTTP请求变慢;
(1)Cookie的一些特性
- Cookie
存储在客户端
,但是服务端和客户端
都可以对其进行读取
、设置
、删除
操作; - Cookie分为
会话Cookie
和持久性Cookie
,前者是没有设置过期时间
,只会在当前会话生命周期内存在,也就是说关掉当前页面,cookie就没了,后者则是设置了过期时间
,只有超过过期时间使时,才会清除; - Cookie是以
文本文件
的格式进行存储的,查看读取十分方便,所以说最好不要
放重要信息
,否则可能会被窃取;
(2)Cookie的一些属性
(1) domain
(域)
告知了浏览器哪些域
可以访问cookie,如果未指定,那就默认当前域,只有当前域
的cookie才能被访问到,发起HTTP请求时,只有和请求地址相同的域或者子域
的cookie才会被携带。
(2)path
(路径)
此属性指定访问 cookie 的路径。除了将 cookie 限制到域之外,还可以通过路径来限制它。 路径属性为 Path=/store 的 cookie 只能在路径为/store
或者它的子路径(/store/a)
的请求中被携带访问。
(3)Expires
/max-age
(过期时间)
Expires
是一个具体的时间,max-age
是一个单位为秒
的时间段,都代表了cookies的过期时间
,超过了这个时间之后,cookie就会过期,不会在请求之间携带。
(4)secure
该属性规定了cookies只能
通过https协议
进行传输,值的类型为boolean
或null
。
(5)http-only
该属性规定了只有
服务端可以访问
或者通过请求头去设置
cookie,而客户端只能进行发送
,但是不能进行访问
。
(6) samesite
该属性有三个值Strict
、Lax
、None
,
- 第一个值代表
严格的
,完全禁止第三方cookie
,只有当请求地址
和当前页面
同域才能携带cookie; - 第二个值代表
相对宽松
,和第一种类似,但是导航到源站点的get请求除外,这种情况依然会携带cookie
; - 第三种表示
非常宽松
,无论是跨域还是同域,都会携带cookie
,如果设置为None,必须
设置secure
属性,否则会被视为设置为lax
来处理;
Storage
storage
分为localStorage
和sessionStorage
,前者除非手动清除,否则会永久存在
,
后者只保存在当前会话中
。
- localStorage和sessionStorage存储的键值对总是以
字符串
的形式储存,如果存储的信息是一个对象或者数字,也会转换成字符串类型; - localStorage和sessionStorage存储的大小
最多
只有5M
;
Storage的操作方法
localStorage.setItem('name', 'Lee') // 给localstorage添加属性
localStorage.getItem('name') // 获取某个属性
localStorage.removeItem('name') // 删除某个属性
localStorage.clear() // 清空storage
IndexedDB
- 它是为了解决存储
大量
的结构化数据而产生的,如果只是少量数据的话,直接使用Storage
即可; - 它是一个
事务型数据库系统
; - 它的执行操作是
异步
的,以免阻塞应用程序; 关于IndexedDB的具体使用可参考MDN
15. Cookie和Session的区别
- 二者都是为了解决
HTTP的无状态
而产生的; - Cookie是保存在
客户端
的,而Session是保存在服务端
的;
16. Cookie和Storage的区别
- Cookie会在发送网络请求时通过请求头
发送给服务器
,而Storage只会保存在本地
; - Cookie可以被
客户端
和服务端
进行修改,而Storage只能被客户端进行修改
; - Cookie支持的最大容量为
4Kb
,而Storage支持最大容量为5M
; - Cookie如果
没有
设置过期时间
,就只在当前会话有效期内有效,这一点和sessionStorage相似
,但是localStorage在清除之前
永久存在; - Cookie如果
设置
了过期时间
,就会在时间有效期内有效,而Storage对象则依旧保持会话有效期/永久生效;
17. 浏览器渲染之——回流和重绘
(1)什么是回流
回流
又称重排
,它指的是我们修改
或者访问
了一些HTML元素
的几何信息
,比如宽高
等,当我们操作了这些之后,浏览器就需要重新计算每一个元素的布局位置
等,重新生成布局树,然后进行分层分块光栅化
等一系列操作,频繁的操作会引起页面的卡顿,影响页面性能。
(2)什么是重绘
重绘
指的是没有修改元素的几何信息
,只是修改了外观
,例如背景颜色
、字体颜色
等,这些操作不会引起浏览器重新计算布局,但是需要浏览器重新进行绘制操作,相对于回流来说,性能会稍好一些
。
(3)如何减少回流/重绘的产生
为了我们的页面能拥有更好的性能,通常我们会尽量减少
回流和重绘的发生,可以通过以下方式:
- 操作dom修改样式时,尽量修改元素的
class
代替修改元素的style
,将多个style
在一个class
中完成; - 将一些复杂的动画效果运用到
position
为fixed
或者absolute
的元素上,这些元素会脱离文档流
,不会影响其他元素的布局; - 对DOM进行
离线处理
,在对DOM进行多次影响布局的操作时,可以先将DOM的display
设置为none
,然后处理完毕再进行展示,就会在一次回流中处理完毕; - 利用
GPU加速
,也就是多采用transform
代替left
、right
等操作,因为transform
是在浏览器渲染的最后一步交由GPU硬件
来进行绘制的,性能会更好,也不会引起回流重绘,此外opacity
、filters
也是;
18. DOM和BOM
DOM
就是我们HTML文档
解析完毕,由各个节点组成的DOM树
;BOM
就是Browser Object Model
,也就是我们的浏览器对象,比如window
对象,document
对象,以及这些对象上的一些方法,都属于BOM
;DOM
和BOM
并不是同一个东西,在我们的开发中,通常我们操作DOM去访问文档内容
,然后通过BOM
去操作我们浏览器的一些功能,二者相辅相成,缺一不可;
19. 事件传播阶段(事件模型)
事件传播
分为三个阶段,顺序依次是捕获 -> 目标 -> 冒泡
:
(1)捕获阶段
:从window
开始,向事件的触发处
传播,但是此时不会触发事件
,因此到达目标父级
,停止;
(2)目标阶段
:事件到达目标元素,触发目标事件;
(3)冒泡阶段
:然后从目标开始,向上传播,遇到注册的冒泡事件
会触发;
事件冒泡
在事件传播的过程中,子元素的事件会依次向父级传播
,由内向外
传播,就称之为事件冒泡
;
这就会造成,我们明明想触发父元素的事件,但是子元素的事件也同时被触发
;
事件捕获
事件传播的过程中,父元素的事件会向子元素传播
,由外向内
传播,就称之为事件捕获
;
如何取消事件冒泡
IE浏览器
可以在处理事件回调函数中添加e.cancelBubble = true
来取消事件冒泡;
符合W3C标准的浏览器
可以在处理事件回调函数中添加e.stopPropagation()
来取消事件冒泡;
如何取消默认事件
某些标签拥有默认事件
,比如a标签
、form表单
等,如果我们想阻止这些元素的默认事件,可以通过以下方式进行解决:
正常的浏览器
可以通过e.preventDefault()
;
IE
则需要通过e.returnValue = false
;
20. V8引擎
(1) V8引擎是什么
V8
是由C++
编写的JavaScript 和 WebAssembly 引擎
,也正是有了V8,chrome浏览器
才会在众多浏览器中脱颖而出
;V8
可以独立运行
,也可以嵌入到
任何C++应用程序中,Node.js就是基于V8引擎的
;
V8引擎对js代码的处理流程大概如下:
-
V8 引擎解析 JavaScript 代码时,首先会将代码通过
Parser
进行词法分析
和语法分析
解析为AST抽象语法树
(一个查看 AST 语法树的网站),vue 中的template
以及babel
都使用了AST抽象语法树
; -
然后会通过
Ignition
是一个解释器
,将AST抽象语法树
转化为字节码
,同时会帮助Turbofan
收集一些优化所需要的信息(比如函数的参数类型信息,函数的调用次数等); -
之后将
字节码
转化为机器码
被计算机识别并执行,(为什么不直接转化为机器可以识别的机器码呢?因为不同的机器拥有不同的 CPU,不同架构的 CPU 所支持的机器指令不同,因此不能直接转化为机器指令,而是逐步转换); -
Turbofan
是一个编译器
,一些高频的代码,它会对其进行标记(hot
),把这些代码的机器指令保存下来
,再次执行时,无需转换字节码,可直接执行机器指令
,这也是 V8 引擎快速的原因之一; -
在执行
Turbofan
中保存的函数时,如果函数参数类型发生改变,函数的执行流程可能有所不同,因此会通过deoptimization
进行反向优化, 把机器码重新转化成字节码再次进行编译运行;
(2)JavaScript中的内存管理
- 在JavaScript中,如果是
基本数据类型
,V8会帮助我们在栈空间
分配内存;- 如果是
复杂数据类型
,会帮助我们在堆空间
分配内存,并将对该变量的引用指向该块堆空间
,但是该块空间的内存地址
,存在栈空间
中;- 访问一个对象时,会先去
栈空间
找它的引用地址,然后再去堆空间
找到数据;
(3)垃圾回收
因为内存空间大小是
有限的
,因此一些数据如果在不使用的时候不进行回收或销毁,就会一直占用内存,如果数据太多,就会出现栈溢出
的报错,因此,当一些数据不再需要时
,我们应对其进行回收
,垃圾回收器(Garbage Collection)也称作GC
。
(4)常见的垃圾回收算法
引用计数
:当一个变量被引用
时,内部就会记录它的引用次数
,当引用次数为0
时,就会被垃圾回收器回收掉;它的弊端也很明显,如果两个变量相互引用
,那么计数就永远不会是0,内存就永远不会被回收,造成内存泄漏
问题;标记清除
:设置一个根对象(Root Object)
,垃圾回收器会定期从根对象开始查找,如果某些对象没有被引用到
,就会把这些对象进行垃圾回收,该算法可以很好的解决循环引用问题
,大多数js引擎采用该方法,而V8为了更好的优化,还搭配了一些其他算法使用;
(5)V8引擎中的垃圾回收
栈内存的回收
在js中,由于js是单线程的,每次执行函数的过程就是一个压栈
的过程,在V8中,每次执行一个函数时,就会有一个ESP指针
指向当前正在执行的上下文,当执行完毕
时,指针就会向下移动
,上一个执行上下文的内存就会被回收
,局部活动对象也就会随之销毁。
堆内存的回收
- V8引擎对于
堆内存
中的变量,将它们分为了两种,分别是新生代
和老生代
; 新生代
指的就是那些存活时间比较短
的对象,这个区域大小通常大小在1~8M
左右;老生代
指的就是那些存活时间比较长
的对象,这个区域容量相对比较大;- V8为新生代和老生代又使用了了
两个
主要的垃圾回收器,分别是副垃圾回收器
,负责新生代区域的垃圾回收,以及主垃圾回收器
,负责老生代区域的垃圾回收;
新生代垃圾回收策略
- 新生代采用
Scavenge
算法对新生代区域分区
,分成使用区(From)
和空闲区(to)
; - 当创建一个
新的对象时
,会先
被分配到使用区
; - 当
使用区空间到达阈值
后,会进行垃圾回收
,垃圾回收器对使用区的活跃对象
进行标记,然后把标记过的对象
复制到空闲区
,然后清除使用区
,然后再将空闲区和使用区进行交换
; - 当一个对象
多次复制且依旧存活时
,就会被认为是一个存活时间较长的对象
,于是就会从新生代
转移到老生代
(当从使用区复制一个对象至空闲区时,如果空闲区的使用空间超过25%
,那么该对象就不会
被复制到空闲区,而是直接转移到老生代
,如过不这么操作,比如所有使用区的对象都处于活跃状态,全都转移到了空闲区,然后又全都转移到了使用区,这时,如果新增一个活跃对象,使用区就没有它的存储位置了);
老生代垃圾回收策略
- 老生代采用了
标记清除
和标记整理
两种算法; 标记清除
就是定期从根节点查找那些没有活跃的变量,进行清除,但是这种清除方法会出现内存碎片现象
;标记整理
就是为了解决标记清除之后产生的内存碎片化现象
;
内存碎片化现象指的就是,不同的变量它们在内存中的位置可能是
断断续续的
,我们 在回收它们的时候,空出来的空间
也变成了断断续续的,这时如果老生代新进入了一个占用内存较大的对象
,我们发现这些空闲出来的空间都不够插入
的,直到找到有足够空间的位置
才行,就会造成许多不必要的查找,而标记整理
就是在我们回收掉这些对象之后,把这些空闲空间由后面的变量挤占过去,那就形成了一部分空间都是完美利用的,另一部分都是空闲出来的。
其他垃圾回收的优化
并行回收和增量标记
由于js是
单线程
的,而垃圾回收也由主线程去完成,如果垃圾回收时间较长
,就会阻塞js脚本运行
,导致系统停顿
,等待垃圾回收完成之后
,才能恢复正常,这种现象称为全停顿
。
为了解决这种现象,V8引擎首先
加入了并行回收
的策略,也就是在进行垃圾回收时,会开启多个辅助线程
进行协助处理,缩短了垃圾回收的时间,但是在处理老生代
垃圾回收时,效果依旧不是特别理想,于是又提出了增量标记的策略进行优化
(采用三色标记法
),主要有以下几个步骤:
初始标记
:在初始节点,V8引擎会从根对象
出发,依次遍历能由根对象直接可达的对象
,并将它们标记为活跃对象
,此时会阻塞
脚本的执行;并发标记
:在标记出活跃对象之后,V8会启动辅助线程,异步遍历其它的对象
,并找到活跃的对象
,这个阶段不会阻塞
脚本执行;再标记
:在辅助线程标记活跃对象的过程中,由于脚本代码还在执行
,可能某些对象的引用已经改变了,或者说已经不再活跃了,就需要对并发标记过程中标记的活跃对象
再次进行标记,以保证引用的准确性
;开始清除
:在增量标记完成之后,V8引擎会开始垃圾回收,回收的过程会使用惰性回收
和并发回收
;
通过这种方式,V8引擎将一次GC
的过程分解成了多步进行
,其中有同步又有异步
,即能准确高效的清除非活跃对象
,又不会长时间阻塞脚本执行
。
惰性回收
惰性回收
指的是V8引擎最终在执行垃圾回收
的时候,如果发现当前内存还够用
,那就延迟
进行回收,以保证js代码的优先执行
,然后再进行清理,清理也不会一次性清理
,而是清理一部分,剩余的留在下次GC
再进行清除。(说白了就是,只要内存够用就先不清除,先保证代码的流畅运行,找机会再清除。)
并发回收
虽然引入了惰性回收
和增量标记
,可以使GC的效率大大增强,但是最终在进行回收的时候,还是会引起主线程JS脚本的停顿
,为了继续优化这方面,V8引擎又引入了并发回收
,也就是说在最终回收阶段,开启辅助线程
,帮助主线程进行垃圾回收,这样既完成
了垃圾回收,又不会
阻塞主线程js脚本的执行。
21. 浏览器的安全性问题
(1)XSS攻击
XSS攻击
就是跨站脚本攻击
,指那些通过在HTML文档中嵌入js脚本
的方式,从而获取到用户的私密信息的操作,它主要有三种,分别是存储型
、反射型
、文档型
。
存储型
:一般指一些输入框没有做js脚本的校验
,导致黑客输入了一些恶意脚本
,然后上传至服务器
,然后客户端拿到这些数据又进行了执行;反射型
:一般指将恶意脚本
作为网络请求的参数
发送给服务器,服务器解析之后拼接到HTML文档中
发送给客户端,客户端进行执行了这些恶意脚本;DOM-based型
:比如一些由用户手动输入然后生成DOM
的网站,攻击者将恶意脚本
注入到页面中,用户输入内容时,恶意脚本篡改内容,最终执行了恶意脚本,达到了攻击的效果。
如何防止XSS攻击
- 我们可以通过
严格校验用户输入内容
,过滤
一些可能发生风险的输入内容,对一些有特殊含义的字符
进行转义
; 利用HttpOnly
,不允许通过浏览器访问cooike,以阻止XSS攻击窃取cookie中的敏感信息;使用CSP(浏览器安全策略)
:只允许当前页面加载指定的
外部资源,可以通过设置http header中的Content-Security-Policy
或者meta标签中的http-equiv属性
。
(2)CSRF
CSRF
就是跨站请求伪造
,黑客诱导用户点击链接进入第三方站点,然后通过用户的cookie信息发起而已请求;
如何防止CSRF攻击
设置samesite
:通过设置cookie的samesite属性
,阻止
向第三方站点携带cookie信息;服务端验证额外信息
:比如可以给客户端token,然后每次发起请求时让客户端携带token
;阻止第三方页面发起请求
:可以让服务端阻止第三方网站请求接口;
(3)点击劫持
点击劫持
是一种视觉欺骗
手段,攻击者将被攻击者的网站
通过iframe
嵌入到自己的网页中,并且将iframe设置透明
,然后放出一个按钮诱导用户点击
;
如何预防点击劫持
X-FRAME-OPTIONS
是一个 HTTP
响应头,在现代浏览器有一个很好的支持。这个 HTTP
响应头 就是为了防御用iframe
嵌套的点击劫持攻击。
该响应头有三个值可选,分别是
DENY
,表示页面不允许通过iframe
的方式展示SAMEORIGIN
,表示页面可以在相同域名下通过iframe
的方式展示ALLOW-FROM
,表示页面可以在指定来源的iframe
中展示
(4)SQL注入
SQL注入
就是利用前端输入框校验漏洞
以及后端SQL语句漏洞
达到攻击目的,比如攻击者可以在输入框输入SQL命令,然后发送给服务端,服务端将SQL命令
作为前端参数进行解析执行,可能会执行一些非自己意愿的SQL语句。
如何预防SQL注入
- 前端严格对输入框内容进行校验,将一些
特殊含义的字符
进行过滤
; - 服务端对
前端参数
也要严格进行校验
,防止一些非法参数的混入;
XSS和CSRF以及SQL注入的区别
XSS
和SQL
注入都是因为校验问题产生的漏洞,不过前者是前端执行恶意脚本
,导致用户信息泄露产生的,而后者是因为后端执行恶意SQL
而产生的漏洞;XSS
是通过先获取到用户的信息
再进行攻击,而CSRF
则无需获取用户信息
,直接利用http携带cookie
的特性,直接发起攻击,而不知道cookie中的具体内容
;XSS
是执行了恶意脚本
,才导致的被攻击,而CSRF无需执行
恶意脚本,只是利用http本身的漏洞和开发者配置的漏洞进行的攻击;
总结:大多数的浏览器安全性问题,其实都是可以避免的,这需要我们前后端在开发时,都要做到严格的校验
,确保传参
和出参
的安全性,包括私密信息的加密等,都非常重要。