文章目录
部分内容摘自MDN(HTTP Headers)
参考文章DOM事件模型三(关于Dom0级和Dom2级)
参考文章彻底弄懂强缓存与协商缓存
参考文章浏览器架构(梳理、总结)
参考文章(2.4w字,建议收藏)😇原生JS灵魂之问(下), 冲刺🚀进阶最后一公里(附个人成长经验分享)
一、事件模型
浏览器事件模型
- DOM 0级事件模型
- DOM 2级事件模型
DOM 0级事件模型
有两种处理方式:
- 直接将事件挂载在html元素属性上
- 通过js将事件添加到html元素属性上
<button onclick="btnClick()"></button>
需注意,DOM 0级事件模型,只允许监听一个事件,后面监听的事件会覆盖前面的事件
DOM 2级事件模型
DOM 2级事件模型分为三个阶段:
- 捕获阶段:
事件从Document对象沿着文档树向下传播给节点。如果目标的任何一个祖先专门注册了事件监听函数,那么在事件传播的过程中就会运行这些函数。(0级DOM事件模型处理没有捕获阶段) - 目标阶段:
下一个阶段发生在目标节点自身,直接注册在目标上的适合的事件监听函数将运行。(一般将此阶段看作冒泡阶段的一部分) - 冒泡阶段:
这个阶段事件将从目标元素向上传播回Document对象(与捕获相反的阶段)。虽然所有事件都受捕获阶段的支配,但并不是所有类型的事件都冒泡
DOM2级事件绑定是使用addEventListener
方法(IE使用attachEvent
方法)
addEventListener
的第三个参数默认为false ,即事件在冒泡阶段调用。当为true 时,则事件在捕获阶段调用;
浏览器会给当前元素的某个事件行为开辟一个事件池(事件队列)【浏览器有一个统一的事件池,每个元素绑定的行为都放在这里,通过相关标志区分】,当我们通过
addEventListener/attachEvent
进行事件绑定的时候,会把绑定的方法放在事件池中;
当元素的某一行为被触发,浏览器回到对应事件池中,把当前放在事件池的所有方法按序依次执行
使用addEventListener
添加的事件监听,移除时需使用removeEventListener
方法,且其参数需与addEventListener
的参数完全一致
与DOM0级不同的是,使用addEvenListener
可以为当前元素的某一事件行为绑定多个不同方法,同样的,可以使用removeEventListener移除当前元素的某一事件行为的多个不同方法。
需要注意的是,事件处理函数若是匿名函数,则无法被移除
二、 缓存策略
HTTP缓存分为本地缓存及协商缓存
缓存的过程:
- 浏览器请求某一资源时,会先获取该资源缓存的
header
信息,然后根据header
中的Cache-Control
和Expires
来判断是否过期。若没过期则直接从缓存中获取资源信息,包括缓存的header的信息,所以此次请求不会与服务器进行通信. - 如果显示已过期,浏览器会向服务器端发送请求,这个请求会携带第一次请求返回的有关缓存的header字段信息,比如客户端会通过
If-None-Match
头将先前服务器端发送过来的Etag
发送给服务器,服务会对比这个客户端发过来的Etag
是否与服务器的相同,若相同,就将If-None-Match
的值设为false
,返回状态304,客户端继续使用本地缓存,不解析服务器端发回来的数据,若不相同就将If-None-Match
的值设为true
,返回状态为200,客户端重新接收服务器端返回的数据;客户端还会通过If-Modified-Since
头将先前服务器端发过来的最后修改时间戳发送给服务器,服务器端通过这个时间戳判断客户端的页面是否是最新的,如果不是最新的,则返回最新的内容,如果是最新的,则返回304,客户端继续使用本地缓存
本地缓存(强缓存)
Exprise
Expires
响应头包含日期/时间, 即在此时候之后,响应过期
Cache-Control
Cache-Control
通用消息头字段,被用于在http请求和响应中,通过指定指令来实现缓存机制
可缓存性:
public
表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存.
即使是通常不可缓存的内容。(例如:1.该响应没有max-age
指令或Expires
消息头;2. 该响应对应的请求方法是POST
。)private
表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。
私有缓存可以缓存响应内容,比如:对应用户的本地浏览器no-cache
在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证(协商缓存验证)no-store
缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存
到期设置:
max-age=<seconds>
设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。s-maxage=<seconds>
覆盖max-age或者Expires头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它max-stale[=<seconds>]
表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间min-fresh=<seconds>
表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应stale-while-revalidate=<seconds>
表明客户端愿意接受陈旧的响应,同时在后台异步检查新的响应。秒值指示客户愿意接受陈旧响应的时间长度stale-if-error=<seconds>
表示如果新的检查失败,则客户愿意接受陈旧的响应。秒数值表示客户在初始到期后愿意接受陈旧响应的时间
重新验证和重新加载:
must-revalidate
一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求proxy-revalidate
与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略immutable
表示响应正文不会随时间而改变。资源(如果未过期)在服务器上不发生改变,因此客户端不应发送重新验证请求头(例如If-None-Match或If-Modified-Since)来检查更新,即使用户显式地刷新页面
其他:
no-transform
不得对资源进行转换或转变。Content-Encoding
、Content-Range
、Content-Type
等HTTP头不能由代理修改.only-if-cached
表明客户端只接受已缓存的响应,并且不要向原始服务器检查是否有更新的拷贝
协商缓存
-
ETag
HTTP响应头是资源的特定版本的标识符。这可以让缓存更高效,并节省带宽**,因为如果内容没有改变,Web服务器不需要发送完整的响应。而如果内容发生了变化,使用ETag有助于防止资源的同时更新相互覆盖 -
Last-Modified
是一个响应首部,其中包含源头服务器认定的资源做出修改的日期及时间。 它通常被用作一个验证器来判断接收到的或者存储的资源是否彼此一致。由于精确度比ETag
要低,所以这是一个备用机制(精确到秒)
协商缓存的过程:
- 请求资源时,把用户本地该资源的
etag
同时带到服务端,服务端和最新资源做对比。 - 如果资源没更改,返回304,浏览器读取本地缓存。
- 如果资源有更改,返回200,返回最新的资源
注意response header中的etag、last-modified在客户端重新向服务端发起请求时,会在request header中换个key名,如下:
// response header
etag: '5c20abbd-e2e8'
last-modified: Mon, 24 Dec 2018 09:49:49 GMT
// request header 变为
if-none-matched: '5c20abbd-e2e8'
if-modified-since: Mon, 24 Dec 2018 09:49:49 GMT
三、浏览器架构
进程和线程
- 进程
进程作为资源分配的最小单位,简单来说,进程就是一个程序的运行实例,当你运行一个程序的时候,系统会为这个程序分配一个单独的内存空间,存放程序代码、运行数据、包括一个主线程,我们把这样一个环境成为进程 - 线程
线程,是不可以脱离进程而存在的。线程是 CPU 调度的基本单位,它是比进程更小的能够独立运行的基本单位。一个进程中可以包含 N 多个线程
两者的联系及区别:
- 线程不能脱离进程单独存在,一个进程可以包含多个线程
- 进程拥有独立的内存空间,而线程共享当前进程的内存空间
- 一个线程可以创建及销毁另外一个线程,同一个进程中可以存在多个线程并发执行。
- 进程相互隔离,数据共享相对困难,进程间的通信成为
ICP
通信。而线程可以访问同一进程的数据,因此需要考虑上锁情况。 - 不同进程间,一个进程的崩溃不会影响其他进程。一个线程的奔溃会导致该进程的奔溃。
- 进程比线程消耗更多的计算机资源。
- 进程关闭时,系统会回收整块内存
浏览器架构版本
早期单进程版本
只有一个进程,数据及文件,网络线程、页面线程等都在一个进程内;进程的任务繁重;
- 不稳定
- 不安全
- 不流畅
早期多进程版本
分为浏览器主进程、多个渲染进程以及多个插件进程等,进程之间采用 IPC
的方式进行通信
- 解决不稳定问题
不同进程崩溃只会影响当前的进程; - 解决不流畅问题
多进程浏览器的渲染进程彼此都跑着自己的程序,互不干扰,减少了阻塞,且对应进程关闭时,浏览器回收了内存; - 解决不安全的问题
多进程浏览器引入了沙箱(sandbox)的概念,渲染进程和插件进程都运行在沙箱中,被限制了直接读取和写入操作系统的能力,这也避免了程序对操作系统进行恶意的攻击,保障了安全性
当前多进程版本
在原来基础上,从浏览器主进程中抽离出了网络进程、GPU 进程、V8 代理解析工具等
打开一个标签页最少需要4
个进程
- 浏览器进程负责用户交互、页面展示、管理子进程等工作
- 渲染进程负责界面的渲染解析工作,以及 javaScript 的执行等。
- 网络进程负责网络请求、下载工作。
- GPU 进程最初则是为了 CSS 3D 效果,后面演化为 UI 界面的绘制也都依赖 GPU 进程来进行。
缺点:进程太多内存占用高。
面向服务架构
面向服务的架构将模块抽象为服务,提供接口,通过 IPC
进行通信,达到了松耦合、高可维护性以及高扩展性的效果。在资源有限的情况下,浏览器会灵活的整合服务到一个进程中,避免资源浪费
四、浏览器工作原理
参考文章:浏览器的工作原理:新式网络浏览器幕后揭秘
五、内存机制
内存存储
正常情况下,基本数据类型用
栈
存储,引用类型数据用堆
存储,但闭包变量是存在堆内存中
基本数据类型: boolen、null、undefined、string、number、symbol、bigint
栈:由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈,使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
堆: 是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些
V8引擎垃圾内存回收
V8 内存限制
64
位系统下,V8最多只能分配1.4G
,32
位系统下,V8最多只能分配0.7G
。(可以通过命令配置)
新生代内存的回收
V8 把堆内存分成了两部分进行处理——新生代内存和老生代内存。顾名思义,新生代就是临时分配的内存,存活时间短, 老生代是常驻内存,存活的时间长。V8 的堆内存,也就是两个内存之和
新生代内存默认限制是多少?在 64
位和 32
位系统下分别为 32MB
和 16MB
首先将新生代内存空间一分为二:
- From部分:正在使用的内存
- To部分: 目前闲置的内存
当进行垃圾回收时,V8 将From部分的对象检查一遍,如果是存活对象那么复制到To内存中(在To内存中按照顺序从头放置的),如果是非存活对象直接回收即可。
当所有的From中的存活对象按照顺序进入到To内存之后,From 和 To 两者的角色对调,From现在被闲置,To为正在使用,如此循环。
新生代垃圾回收算法也叫Scavenge算法
不过Scavenge 算法的劣势也非常明显,就是内存只能使用新生代内存的一半,但是它只存放生命周期短的对象,这种对象一般很少,因此时间性能非常优秀
老生代内存的回收
新生代中的变量如果经过多次回收后依然存在,那么就会被放入到老生代内存中,这种现象就叫晋升
触发晋升的两种情况:
- 已经经历过一次 Scavenge 回收
- To(闲置)空间的内存占用超过25%
老生代回收策略
- 第一步,进行标记-清除。这个过程在《JavaScript高级程序设计(第三版)》中有过详细的介绍,主要分成两个阶段,即标记阶段和清除阶段。首先会遍历堆中的所有对象,对它们做上标记,然后对于代码环境中使用的变量以及被强引用的变量取消标记,剩下的就是要删除的变量了,在随后的清除阶段对其进行空间的回收。
- 第二步,整理内存碎片。
V8
的解决方式非常简单粗暴,在清除阶段结束后,把存活的对象全部往一端靠拢(解决存活对象的空间不连续对后续的空间分配造成障碍的问题)
垃圾清除的办法
-
标记清除:
JavaScript
最常用的垃圾收集方式。当变量进入环境时,这个变量标记为“进入环境”;而当变量离开环境时,则将其标记为“离开环境”。可以使用一个“进入环境”的变量列表及一个“离开环境”的变量列表来跟踪变量的变化,也可以翻转某个特殊的位来记录一个变量何时进入环境及离开环境
V8
采取了增量标记的方案,即将一口气完成的标记任务分为很多小的部分完成,每做完一个小的部分就"歇"一下,就js应用逻辑执行一会儿,然后再执行下面的部分,如果循环,直到标记阶段完成才进入内存碎片的整理上面来 -
引用计数:
不太常见的垃圾收集策略。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则该值的引用次数就是1;如果同一个值又被赋给另一个变量,则该值的引用次数加1;如果包含对该值引用的变量又取得了另外一个值,则该值的引用次数减1。当该值的引用次数变为0时,则可以回收其占用的内存空间。当垃圾回收器下一次运行时,就会释放那些引用次数为0的值所占用的内存。该办法存在缺点:循环引用,对象A有指针 指向对象B, 对象B有指针指向对象A;
内存泄漏
导致内存泄漏的几种情况:
- 全局变量
- 闭包
- DOM元素替换或者移除后没有移除监听事件
- 遗漏的定时器和计时器
- IE7/8 引用计数使用循环引用产生的问题