(5k总结,建议收藏🤔)春招必问的浏览器原理,你都会吗?

488 阅读15分钟

这是我参与8月更文挑战的第三天,活动详情查看:8月更文挑战

前言

作为一个合格的前端工程师,浏览器相关的工作原理是我们进行性能优化的基石。所以,在面试中,浏览器相关的知识也是逃不过的必问点。

本文内容会涵盖事件机制跨域问题浏览器渲染过程浏览器本地存储浏览器缓存机制

正文

事件机制

事件的触发过程

  1. 从window上往事件触发处传播,这个过程称为捕获阶段
  2. 处于目标阶段,触发事件
  3. 从事件触发处往window上传播,这个过程为冒泡阶段

阻止事件触发的方法有哪些?

  • 通过addEventListener第三个参数

该参数可以为Boolean值对象

  1. 如果为Boolean:useCapture。默认值为false,表示默认关闭捕获阶段,开启冒泡阶段。改为true,则会开启捕获阶段,关闭冒泡阶段。
  2. 如果为对象,该对象可以使用以下几个属性:
    • capture:布尔值,和 useCapture 作用一样
    • once:布尔值,值为 true 表示该回调只会调用一次,调用后会移除监听
    • passive:布尔值,表示永远不会调用 preventDefault
    注:e.preventDefault() 阻止默认事件的发生 如:阻止掉href的默认跳转事件
  • 通过e.stopPropagatione.stopImmediatePropagation方法来阻止事件
两个方法都可以用来阻止事件(捕获和冒泡都可以)
  • e.stopPropagatione.stopImmediatePropagation 区别

e.stopImmediatePropagation除了能够阻止事件的捕获阶段或冒泡阶段,还能够阻止事件目标上注册的其他的相同类型的事件( 一般来讲,如果一个元素注册了多个点击事件,本来应该是会按照事件注册的先后顺序被触发;当使用了这个方法后,就会使接下来尚未执行的事件不响应

什么是事件代理?

如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上,这种方式称为事件代理

实例代码如下:

<ul id="ul">
	<li>1</li>
    <li>2</li>
	<li>3</li>
	<li>4</li>
	<li>5</li>
</ul>
<script>
	let ul = document.querySelector('#ul')
	ul.addEventListener('click', (event) => {
		console.log(event.target);  //通过访问这个属性,可以拿到点击节点的详细信息
	})
</script>
  • 做这种事件代理的好处?
    1. 节省内存
    2. 不需要给子节点注销事件

跨域问题

什么是跨域?

跨域是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制

  • 出于什么安全考虑才会引入这种机制呢?

其实主要是用来防止CSRF攻击的。CSRF攻击是利用用户的登录态发起恶意请求的,也就是说,如果没有同源策略,A网站可以被任意其他来源的请求访问到内容。如果当前的A网站还处于登录状态,那么对方就可以通过这个请求获取到你的任何信息了。

注:其实同源策略并不能完全阻止SCRF

  • 如果请求跨域了,那这个请求到底有没有被发出去呢?

这个请求其实是发出去了的。浏览器只是拦截了这个请求的响应。 这也解释了为什么通过表单的方式可以发起跨域请求,而Ajax不能。因为究其根本,跨域只是为了阻止用户读取到另一个域名下的内容,Ajax正是因为可以获取响应,浏览器才会拦截掉这个请求。而表单只是通过请求发送数据,并不会从另一个域名获取内容,所以可以发出跨域请求。这也说明了跨域不能完成阻止CSRF的原因:这个跨域请求是发出去了的,只是拦截了响应。

跨域的解决方案

  • jsonp

    jsonp处理跨域的根本其实就是:借助script标签没有跨域限制的漏洞,客户端发送script请求,参数中带着处理返回数据的回调函数,服务端接收到请求并帮助执行掉这个回调函数

  • JSONP函数的简易封装以及jsonp解决跨域的实现:

    <script>
    class Jsonp {
        constructor(req){
            this.url = req.url
            this.callbackName = req.callbackName
        }
        create(){
            //创建一个script标签
            const script = document.createElement('script')
            //将传入的参数进行拼接作为src的url
            const url = `${this.url}?callback=${this.callbackName}`
            script.src = url
            //将script标签插入到html中
            document.getElementsByTagName('head')[0].appendChild(script)
        }
    }

    new Jsonp({
        url:'http://127.0.0.1:3000',
        callbackName:'getMsg'
    }).create()

    function getMsg(data){
        data = JSON.parse(data)
        console.log(`My name is ${data.name}, and ${data.age} year`);
    }
</script>

简单的讲,上述代码相当于在一份html上加上一句:

<script src="http://127.0.0.1:3000?callback=getMsg"></script>

就和引入jQuery框架的效果一样。之后,当浏览器读取到这份HTML,当读到这个script标签时,因为script标签是跨域请求的白名单的规则,最终实现浏览器向http://127.0.0.1:3000的跨域请求

- 优点:
    1. 使用简单
    2. 兼容性不错
- 致命缺点:
    1. 因为该方式是借助script脚本请求方式,所以只适用于get请求,只能传输较小的数据。且安全性不高,容易被注入
  • cors (跨域资源共享)

    首先浏览器会对请求做预检,判断是简单请求还是复杂请求。如果是简单请求,该请求会携带Origin,如果该origin不在服务端规定的Access-Control-Allow-Origin中 ,浏览器就会拦截响应;如果是复杂请求,首先浏览器会发起一个预检请求,该请求是option方法的,通过该请求就知道服务端是否允许跨域请求。

    由此可见,通过cors实现跨域请求的根本在于服务器端的配置

    • 浏览器预检:

      1. Access-Control-Request-Method //请求的方法
      2. Access-Control-Request-Headers // 请求的头部
      3. Origin //请求发出的域
    • 服务端应答:

      1. Access-Control-Allow-Origin //规定能发出这个请求的域名
      2. Access-Control-Allow-Methods //允许请求的方法列表
      3. Access-Control-Allow-Headers //允许请求的头部字段
      4. Accrss-Control-Allow-Age //预检请求能被缓存的最长时间,在这个时间内,同一个请求不会再次发出预检请求
    • 简单请求

    以 Ajax 为例,当满足以下条件时,会触发简单请求

      1.  使用下列方法之一:
    
          -   `GET`
          -   `HEAD`
          -   `POST`
    
      2.  `Content-Type` 的值仅限于下列三者之一:
    
          -   `text/plain`
          -   `multipart/form-data`
          -   `application/x-www-form-urlencoded`
    
      请求中的任意 `XMLHttpRequestUpload` 对象均没有注册任何事件监听器; `XMLHttpRequestUpload` 对象可以使用 `XMLHttpRequest.upload` 属性访问。
      
    
    • 复杂请求

      不满足以上条件的,则为复杂请求

  • webSocket
    不是http请求,所以不会跨域 HTML5新增的协议,全双工,使用该方法建立的通信一经连接不会断开

  • nginx

    实现:就是浏览器要对其他服务器发送的请求通过自己的服务器代理发送

    原理:在服务器中配置一个代理做服务器代理,利用服务器之间通信不存在跨域的特性

  • postMessage (H5提供的与第三方页面交互的方法)

    这种方式通常用与嵌入页面的第三方页面进行数据交互。一个页面发送消息,另一个页面判断来源并接收消息

  • document.domain 两个页面都通过js强制设置document.domain = 同一个二级域名为基础主域,就实现了同域

该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。

浏览器渲染过程

浏览器在拿到页面所有的组件(html,css,js,图片)之后,会解析生成两个内部数据结构 -- DOM树 和 渲染树

  1. 解析html,生成DOM树

  2. 解析CSS,生成cssOM树

  3. 生成渲染树

    1. 从DOM树的根节点开始遍历每个可见节点。
    2. 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
    3. 根据每个可见节点以及其对应的样式,组合生成渲染树。
  4. 回流:重新遍历渲染树,根据可见的渲染树节点信息,计算它们在设备视口(viewport)内的确切位置和大小(几何信息)

  5. 重绘:根据经过回流操作(得到几何信息)之后的渲染树的可见节点,得到节点的绝对像素,绘制在浏览器上

回流(重排)

  • 1、什么是回流? —— 重新遍历渲染树,根据可见的渲染树节点信息,计算它们在设备视口(viewport)内的确切位置和大小(几何信息)
  • 2、触发回流的情况:当页面布局和几何信息发生变化的时候,就会发生回流 - 添加或删除可见的DOM元素 - 元素的位置发生变化 - 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等) - 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。 - 页面一开始渲染的时候(这肯定避免不了) - 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

重绘

  • 1、什么是重绘? —— 根据经过回流操作(得到几何信息)之后的渲染树的可见节点,得到节点的绝对像素,绘制在浏览器上
  • 2、触发重绘的情况:非几何元素发生改变的情况

重排和重绘的关系 (重要)

在重排的时候,浏览器会使渲染树种受到影响的部分失效,并重新构造这部分渲染树。完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程即为重绘。因此发生回流一定会触发重绘,重绘不一定发生回流

浏览器本地存储

cookie、localStorage、sessionStorage、indexDB

这几种存储方式的区别:

  • 数据生命周期上
    • cookie 一般由服务器生成,可以设置过期时间
    • localStorage 除非被清理,否则一直存在
    • sessionStorage 页面关闭就会被清理
    • indexDB 除非被清理,否则一直存在
  • 数据存储大小上
    • cookie 4k
    • localStorage 5M
    • sessionStorage 5M
    • indexDB 无限,取决于电脑容量
  • 与服务端通信上
    • cookie 每次都会携带在header中,对于请求性能有影响
    • localStorage 不参与
    • sessionStorage 不参与
    • indexDB 不参与
  • 在作用域上
    • cookie 在所有同源窗口中共享
    • sessionStorage 不能再不同的浏览器窗口间共享,即使是同一个页面
    • localStorage 可在所有同源窗口共享
  • 在使用场景上
    • cookie 一般使用在一段时间内免登录的业务需求上,可以手动设置过期时间
    • localStorage 一般用来存储一些常用的静态资源,如:图片等
    • sessionStorage 最常见的使用场景是:解决页面刷新vuex数据会重置的bug

cookie

  • cookie一般是在服务器端生成,发送给浏览器端,通知浏览器去设置cookie
  • cookie还有path的概念,可以限制cookie只属于某个路径下
  • 四个配置参数:
    • value:即存储的值,这里一般需要加密
    • http-only:不能通过js访问,只有http请求才能访问cookie。 一般会开启,可以避免XSS攻击
    • secure:限定值只能在https的请求中被访问
    • same-site:限定浏览器不能在跨域请求中携带cookie,避免CSRF攻击

Cookie 和 Session 区别

  • cookie 和 session都可用来存储用户信息
  • cookie存放在客户的浏览器上,session存放在服务器上
  • cookie的安全性不高,可能会发生Cookie欺骗,考虑安全的话应使用Cookie
  • session在一定时间内都保存在服务器,当访问增多,会比较占用服务器的性能。考虑减轻服务器性能方面,应使用cookie

浏览器缓存机制

image.png 接下来以三个部分来总结:

  • 强缓存
  • 协商缓存
  • 缓存位置

强缓存

首先是检查强缓存,这个阶段不需要发送HTTP请求

怎么检查?通过相应的字段来进行。

HTTP/1.0HTTP/1.1当中,这个字段是不一样的。在早期,也就是HTTP/1.0时期,使用的是Expires,而HTTP/1.1使用的是Cache-Control。让我们首先来看看Expires。

Expires

Expires即过期时间,存在于服务端返回的响应头中,告诉浏览器在这个过期时间之前可以直接从缓存里面获取数据,无需再次请求。

注意:这里存在一个坑:服务器时间和浏览器时间可能不一致。服务器返回的这个过期时间可能就是不准确的。因此这种方式很快在后来的HTTP1.1版本中被抛弃了。

Cache-Control

在HTTP1.1中,开始采用该关键字段:Cache-Control

它和Expires本质的不同在于它并没有采用具体的过期时间点这个方式,而是采用过期时长来控制缓存,对应的字段是max-ageCache-Control:max-age=3600 代表这个响应返回后在 3600 秒,也就是一个小时之内可以直接使用缓存。

其次,该字段还可以组合非常多的指令,完成更多场景的缓存判断:

  • public:表示客户端和代理服务器都可以缓存。因为一个请求可能会经过多个代理服务器最后才到达目标服务器。这样的话,不仅仅浏览器可以缓存数据,中间的任何代理节点都可以进行缓存。
  • s-maxage:这和max-age长得比较像,但是区别在于s-maxage是针对代理服务器的缓存时间。
  • private: 这种情况就是只有浏览器能缓存了,中间的代理服务器不能缓存。
  • no-cache: 跳过当前的强缓存,发送HTTP请求,即直接进入协商缓存阶段
  • no-store:非常粗暴,不进行任何形式的缓存

那么,讨论完强缓存,如果资源缓存时间超时,也就是强缓存失效了怎么办?进入第二级 —— 协商缓存

协商缓存

强缓存失效之后,浏览器在请求头中携带相应的缓存tag来向服务器发请求,由服务器根据这个tag,来决定是否使用缓存,这就是协商缓存

具体来说,这样的缓存tag分为两种: Last-Modified 和 ETag。这两者各有优劣

Last-Modified

表示为最后修改时间。

当浏览器第一次给服务器发请求时,服务器会在响应头中携带该字段

浏览器接收后,当浏览器再次向服务器发送时,会在请求头中携带If-Modified-Since字段,这个字段的值也就是服务器传来的最后修改时间。

服务器拿到请求头中的If-Modified-Since的字段后,其实会和这个服务器中该资源的最后修改时间对比:

  • 如果请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
  • 否则返回304,告诉浏览器直接用缓存。

ETag

ETag 是服务器根据当前文件的内容,给文件生成的唯一标识,只要里面的内容有改动,这个值就会变。服务器通过响应头把这个值给浏览器。

浏览器接收到ETag的值,会在下次请求时,将这个值作为If-None-Match这个字段的内容,并放到请求头中,然后发给服务器。

服务器接收到If-None-Match后,会跟服务器上该资源的ETag进行比对:

  • 如果两者不一样,说明要更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
  • 否则返回304,告诉浏览器直接用缓存

二者对比

  1. 精准度上,ETag优于Last-Modified。优于 ETag 是按照内容给资源上标识,因此能准确感知资源的变化。而 Last-Modified 就不一样了,它在一些特殊的情况并不能准确感知资源变化,主要有两种情况:
    • 编辑了资源文件,但是文件内容并没有更改,这样也会造成缓存失效。
    • Last-Modified 能够感知的单位时间是秒,如果文件在 1 秒内改变了多次,那么这时候的 Last-Modified 并没有体现出修改了
  2. 在性能上,Last-Modified优于ETag,也很简单理解,Last-Modified仅仅只是记录一个时间点,而 Etag需要根据文件的具体内容生成哈希值。

注:如果两种方式都支持的话,服务器会优先考虑ETag

缓存位置

强缓存命中或者协商缓存中服务器返回304的时候,我们直接从缓存中获取资源。那这些资源究竟缓存在什么位置呢? 浏览器中的缓存位置一共有四种,按优先级从高到低排列分别是:

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache

Service Worker

Service Worker 和 Web Worker(让JS运行在主线程之外,不会阻塞线程,并通过postMessage将结果返回传到主线程)的思路相似,即让 JS 运行在主线程之外,由于它脱离了浏览器的窗体,因此无法直接访问DOM。虽然如此,但它仍然能帮助我们完成很多有用的功能,比如离线缓存消息推送网络代理等功能。其中的离线缓存就是 Service Worker Cache

Memory Cache 和 Disk Cache

Memory Cache指的是内存缓存,从效率上讲它是最快的。但是从存活时间来讲又是最短的,当渲染进程结束后,内存缓存也就不存在了。

Disk Cache就是存储在磁盘中的缓存,从存取效率上讲是比内存缓存慢的,但是他的优势在于存储容量和存储时长

  • 比较大的JS、CSS文件会直接被丢进磁盘,反之丢进内存
  • 内存使用率比较高的时候,文件优先进入磁盘

结语