2024前端年末备战面试题——浏览器篇

4,090 阅读37分钟

浏览器篇

本文是本人根据MDN上的解答或者网上一些其他朋友的文章以及自己的理解,整理归纳出来的一篇浏览器方面的面试题,其中不免有许多漏掉的问题或是答案,发现错误的或者是有什么问题需要补充的朋友们,可以在评论区留言,大家虚心交流,一起进步。

1. 进程和线程

进程是cpu资源分配最小单位; 线程是cpu调度最小单位;

进程和线程的关系

  1. 通俗来讲,每一个执行中的程序就可以称之为进程,而一个进程由多个线程组成;
  2. 一个进程任意一个线程出错,就会导致整个进程出错;
  3. 一个进程中的所有线程共享进程中的数据;
  4. 不同进程之间的内容相互隔离
  5. 关闭进程时,进程中的所有线程也会被关闭;

浏览器的进程和线程

现在许多浏览器采用的是多进程、多线程的架构模式,比如chrome,就是一个多进程并且多线程架构的浏览器。

多线程架构的浏览器,打开的每个Tab是一个线程,而多进程的浏览器,打开的每一个Tab,都是一个进程

多进程浏览器的优势

  1. 我们打开的每个tab页,即使卡顿、崩溃,也只是那一个进程会受到影响,其他进程不会受到影响
  2. 现在很多cpu都是多核的,而多进程可以完美的发挥cpu的优势
  3. 浏览器中还有插件进程,同时也保证了不同插件发生崩溃时不会影响到浏览器的使用

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一旦缓存过期,必须向服务器/代理服务器验证资源的有效性

所有属性如下: image.png

image.png

(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)发起请求时缓存的详细过程

  1. 浏览器第一次加载资源,缓存中没有任何内容,直接正常发起请求,服务器除了返回资源文件以外,还会给response header添加该资源的ETag(如果不支持http1.1,则是最后一次修改的时间Last-Modified),浏览器把资源文件和response header及该请求的返回时间一并缓存;

  2. 下一次加载这个资源时,浏览器会先查看请求这个资源的响应头,如果发现这个请求支持强缓存,那么就通过比较这次发起请求的时间和上一次返回数据成功的时间差,如果没有超过cache-control设置的max-age,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件 (如果浏览器不支持HTTP1.1,则用expires判断是否过期);如果未命中强缓存,则看这个请求是否支持协商缓存,如果支持,就在请求头添加If-None—Match(值为响应头中的ETag(如果不支持http1.1的话,就添加If-Modified-Since,值为响应头的Last-Modified),然后向服务器发送请求;

  3. 服务器收到请求后,根据 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生命周期

  1. 注册(register):如果注册成功,那么Service Worker将会被下载到客户端;

  2. 安装(install):安装分为两种情况,

    (1)这是首次启用Service Worker:

    • 首次启用就会直接尝试安装,安装完成后就会直接进入下个生命周期激活

    (2)之前已经存在激活的Service Worker:

    • 如果现有 service worker 已启用,新版本会在后台安装,但仍不会被激活——这个时序称为 worker in waiting
    • 直到所有已加载的页面不再使用旧的 Service Worker(使用旧的Service Worker的页面全部被关闭掉), 才会激活新的 Service Worker;
    • 可以调用skipWaiting()方法,直接激活新的Service Worker,后续所有页面都将被新的Service Worker接管;
  3. 激活(activate):service worker 将立即控制页面,但是只会控制那些在 register() 成功后打开的页面。

访问Service Worker页面的缓存数据

  1. 浏览器打开一个页面;
  2. 为当前页面打开一个新的进程;
  3. 主线程执行当前页面的任务;
  4. 如果没有碰到Service Worker,则不进行任何处理;
  5. 如果碰到了Service Worker,则开启一个Service Worker线程,并记录下当前页面的URL;
  6. 下次再访问这个URL,会自动打开Service Worker线程,然后访问缓存数据

7. 启发式缓存

如果响应头没有任何强缓存相关的属性,但是有Last—Modified属性,那么浏览器默认会采用一个启发式缓存

该缓存通过公式计算出一个时间,在这个时间内,使用强缓存,公式为Last-Modified Time - Date * 0.1 (10%)

8. 浏览器渲染流程

  1. 从上到下解析HTML,生成DOM树(如果遇到了不带async或者defer的js脚本,会阻塞HTML的解析,如果是带async的js脚本,在加载js脚本时不会阻塞,但是在执行时会阻塞)
  2. 解析CSS资源,生成CSSOM树,CSS的解析不会阻塞HTML的解析,因为CSS的解析是在预解析线程执行,而HTML的解析是在主线程执行,但是会阻塞HTML的渲染;
  3. 将解析好的CSS合并到主线程,将CSSOM树和DOM树合并,构建渲染树(Render);
  4. 依次计算DOM树中每个节点的样式,得到最终样式;
  5. 布局(Layout),将DOM树中的每个节点进行布局,得到布局树;
  6. 进行分层,浏览器会根据布局树,对整个页面进行分层,后续哪些节点有改动,只需要改动某一层的信息,层叠上下文或者CSS属性的will-change都会影响浏览器的分层;
  7. 进行绘制,浏览器发出绘制指令,交给分层之后的每一层进行绘制,至此,主线程工作结束,剩下的交由合成线程进行处理;
  8. 分块,合成线程从线程池中调度更多的线程每个图层进行分块,将他们分成更小的区域
  9. 光栅化,分好块之后,合成现场将分好的块交由GPU线程,GPU会以极快的速度完成光栅化,为了使页面更快的展现出内容,会优先处理靠近视口位置的块;
  10. 画,完成光栅化之后,每个块就变成了一块一块的位图,然后交由合成线程,合成线程将每一块位图画到屏幕相应的位置,并查看是否有缩放旋转等操作,而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

总结

  1. 浏览器开始解析HTML,此时document.readystateloading
  2. 解析中遇到不带asyncdeferscript脚本时,需要等待 script脚本 下载完成并执行后,才会继续解析 HTML
  3. 遇到带deferscript脚本,等待它们下载完毕,这些脚本会在HTML解析之后开始执行,等待它们执行完毕;
  4. 当 HTML 文档完全解析,所有延迟脚本下载和执行完毕document.readyState变成 interactive,触发 DOMContentLoaded事件;
  5. 此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成载入和执行(如<script async src=xxx >),document.readyState变为 completewindow 触发 load 事件;

10. 说一下js文件、css文件、html、图片资源之间的阻塞关系

  • CSS的解析HTML的解析没有阻塞关系,因为二者在不同的线程完成CSS的解析预解析线程HTML的解析主线程
  • CSS的解析阻塞HTML的渲染,因为HTML的渲染需要CSS样式,如果CSS没有解析完毕,HTML无法完成渲染;
  • 不带async或者deferscript标签阻塞HTML的解析,并且会等待执行完毕之后才会继续解析HTML;
  • asyncscript标签在下载时不会影响HTML的解析,因为是在不同的线程进行下载,但是下载完毕之后会立即执行,执行会阻塞HTML的解析;
  • deferscript标签,不会影响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;
  • 不能获取本地存储,如cookiestorageIndexedDB等;

如何解决跨域

  • 跨域资源共享(CORS)
  • jsonp
  • nginx反向代理
  • postMessage跨域
  • 设置代理服务器

CORS解决跨域

跨源资源共享(CORS,或通俗地译为跨域资源共享)是一个基于 HTTP 头的机制,该机制通过允许服务器标记除了它自己之外的其他来源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标明有HTTP器方法而真实请求中会用到的头。

  • 当我们跨域进行发起请求时,如果是简单请求,浏览器就不会进行预检(option)请求,如果是一个复杂请求,就会先发出预检请求
  • 预检请求会根据服务端的响应头中的Access-Control-Allow-origin来判断是否支持跨域,如果支持则正常发送请求,不支持则在控制台报错;
  • 这种方法主要由服务端进行配置,前端无需关注太多;

jsonp进行跨域

jsonp的原理就是利用了script标签不受浏览器同源策略的限制,然后和后端一起配合来解决跨域问题的。

具体的实现方式如下:

  • 在客户端创建一个script标签
  • 把需要请求的接口地址拼接一个回调函数名称作为参数传给服务端,作为script标签的src属性,然后把script标签添加到body中;
  • 当服务端接收到客户端的请求时,会解析得到回调函数名称,然后把数据回调函数名称拼接成函数调用的形式返回给客户端;
  • 客户端拿到该数据之后,进行解析,然后调用该回调函数,在回调函数中就可以拿到服务端返回的数据

nginx反向代理

通过配置nginx文件,把客户端发起的请求代理到真实的服务器地址,然后将服务器返回的数据交给客户端;

postMessage跨域

MDN对postMessage的介绍

window.postMessage()  方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议,端口号,以及主机设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage()  方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

  • 在两个窗口之间,通过window.postMessage()发送消息和数据;
  • 通过window.onmessage来接收postMessage发出的消息和数据;
  • 可以利用该功能使用window.open()打开窗口,然后通过postMessage实现浏览器跨页签通信;

代理服务器跨域

主要是利用了服务器请求服务器不受浏览器同源策略的限制实现的,但是本地和代理服务器之前也会存在跨域问题,只是说如果我们自己有能力搭建代理服务器,我们就可以自己搭建代理服务器,然后实现本地和代理服务器之间的跨域请求,再通过代理服务器和真实服务器之间的请求拿到最终数据,不必再麻烦自己的服务端同事(求人不如求己)。

14. 浏览器存储数据方案

浏览器常见的存储数据的方案一共有四种,分别是CookieLocalStorageSessionStorageIndexedDB

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协议进行传输,值的类型为booleannull

(5)http-only

该属性规定了只有服务端可以访问或者通过请求头去设置cookie,而客户端只能进行发送,但是不能进行访问

(6) samesite

该属性有三个值StrictLaxNone

  • 第一个值代表严格的,完全禁止第三方cookie,只有当请求地址当前页面同域才能携带cookie;
  • 第二个值代表相对宽松,和第一种类似,但是导航到源站点的get请求除外,这种情况依然会携带cookie
  • 第三种表示非常宽松,无论是跨域还是同域,都会携带cookie,如果设置为None,必须设置secure属性,否则会被视为设置为lax来处理;

Storage

storage分为localStoragesessionStorage,前者除非手动清除,否则会永久存在, 后者只保存在当前会话中

  • 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中完成;
  • 将一些复杂的动画效果运用到positionfixed或者absolute的元素上,这些元素会脱离文档流,不会影响其他元素的布局;
  • 对DOM进行离线处理,在对DOM进行多次影响布局的操作时,可以先将DOM的display设置为none,然后处理完毕再进行展示,就会在一次回流中处理完毕;
  • 利用GPU加速,也就是多采用transform代替leftright等操作,因为transform是在浏览器渲染的最后一步交由GPU硬件来进行绘制的,性能会更好,也不会引起回流重绘,此外opacityfilters也是;

18. DOM和BOM

  • DOM就是我们HTML文档解析完毕,由各个节点组成的DOM树
  • BOM就是Browser Object Model,也就是我们的浏览器对象,比如window对象,document对象,以及这些对象上的一些方法,都属于BOM
  • DOMBOM并不是同一个东西,在我们的开发中,通常我们操作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代码的处理流程大概如下:

image.png

  • V8 引擎解析 JavaScript 代码时,首先会将代码通过Parser进行词法分析语法分析解析为AST抽象语法树(一个查看 AST 语法树的网站),vue 中的template以及babel都使用了AST抽象语法树

  • 然后会通过Ignition是一个解释器,将AST抽象语法树转化为字节码,同时会帮助Turbofan收集一些优化所需要的信息(比如函数的参数类型信息,函数的调用次数等);

  • 之后将字节码转化为机器码被计算机识别并执行,(为什么不直接转化为机器可以识别的机器码呢?因为不同的机器拥有不同的 CPU,不同架构的 CPU 所支持的机器指令不同,因此不能直接转化为机器指令,而是逐步转换);

  • Turbofan是一个编译器,一些高频的代码,它会对其进行标记(hot),把这些代码的机器指令保存下来,再次执行时,无需转换字节码,可直接执行机器指令,这也是 V8 引擎快速的原因之一;

  • 在执行Turbofan中保存的函数时,如果函数参数类型发生改变,函数的执行流程可能有所不同,因此会通过deoptimization进行反向优化, 把机器码重新转化成字节码再次进行编译运行;

  • V8 的 Parser 官方文档

  • V8 的 Ignition 官方文档

  • V8 的 Turbofan 官方文档

(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注入的区别

  • XSSSQL注入都是因为校验问题产生的漏洞,不过前者是前端执行恶意脚本,导致用户信息泄露产生的,而后者是因为后端执行恶意SQL而产生的漏洞;
  • XSS是通过先获取到用户的信息再进行攻击,而CSRF无需获取用户信息,直接利用http携带cookie的特性,直接发起攻击,而不知道cookie中的具体内容
  • XSS执行了恶意脚本,才导致的被攻击,而CSRF无需执行恶意脚本,只是利用http本身的漏洞和开发者配置的漏洞进行的攻击;

总结:大多数的浏览器安全性问题,其实都是可以避免的,这需要我们前后端在开发时,都要做到严格的校验,确保传参出参的安全性,包括私密信息的加密等,都非常重要。