垃圾回收
JavaScript 中的内存管理是自动执行的,而且是不可见的.我们创建基本类型 对象 函数……所有这些都需要内存.
- JavaScript中内存管理是自动的
- 对象不再被引用时就是垃圾
- 对象不能从根上访问到时是垃圾
可达对象
JavaScript 中内存管理的主要概念是可达性.
简单地说,“可达性” 值就是那些以某种方式可访问或可用的值,它们被保证存储在内存中.
- 可以访问到的对象就是可达对象(引用,作用域链)
- 可达的标准就是从根出发是否能够被找到
- 根可以理解为全局变量对象
function objGroup(obj1, obj2) {
obj1.next = obj2
obj2.prev = obj1
return {
o1: obj1,
o2: obj2
}
}
let obj = objGroup({name: 'obj1'}, {name: 'obj2'})
console.log(obj)
从根出发,先是找到 obj 对象,然后该对象的 o1 o2 分别指向 obj1 跟 obj2,同时 obj1 的 next 指向 obj2, obj2 的 prev 指向 obj1.当 delete 了 obj.o1 跟 obj2.prev 时,就没有办法通过任何方式来找到这个 obj1,这样 obj1 就会被判定为垃圾然后回收
引用计数算法
设置引用数,判断当前对象引用数是否为0来决定是否为垃圾对象.在引用关系改变时修改引用数字,引用数字为0时立即回收
优点
- 发现垃圾时立即回收
- 最大限度减少程序暂停
缺点
- 无法回收循环引用的对象
- 时间开销大
标记清除算法
分两个阶段,第一个阶段会遍历所有对象,找到可达对象进行标记;第二个阶段也会遍历所有对象,同时清除没有标记的对象和所有标记,方便下一次回收垃圾
优点
- 可以回收循环引用的对象
缺点
- 会导致空间的碎片化
- 不会立即回收垃圾对象
标记整理
可以看做是标记清除的增强,标记阶段的操作和标记清除一致,清除阶段则会先执行整理,移动对象位置让他们地址上产生连续
优点
- 介绍碎片化空间
缺点
-
不会立即回收垃圾对象
V8引擎 -
一款主流的JavaScript执行引擎
-
采用即时编译,即直接把代码转为机器码,而不是先转为字节码再转为机器码,所以速度很快
-
对内存设限 64位下1.5G 32位下 800M
V8垃圾回收策略
- 采用分代回收的思想,内存分为新生代跟老生代
- 针对不同对象使用不同的算法
V8中常用的GC算法
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 标记增量
V8回收新生代对象
- 内存空间一分为二
- 小空间用于存储新生代对象,64位下为32M,32位下为16M
- 新生代对象指的是存活时间较短的对象
新生代对象回收过程
- 回收过程采用复制算法+标记整理
- 新生代内存区分为两个等大小空间,使用空间为 From,空闲空间为 To
- 活动对象存储于 From 空间
- 触发GC后,先通过标记整理将活动对象拷贝到 To
- 释放 From 空间,然后 From 跟 To 区域互换
在从 From 拷贝到 To 中时可能会出现晋升的情况.晋升指的是将新生代对象移动至老生代,通常一轮 GC 后还存活的新生代会晋升,或者 To 空间的使用率超过 25% 的时候也会晋升本轮新生代
V8回收老生代对象
- 内存空间一分为二
- 右侧大空间用于存储老生代对象,64位下为1.4G,32位下为700M
- 新生代对象指的是存活时间较长的对象
老生代对象回收过程
- 主要采用标记清除 标记整理 增量标记算法
- 首先使用标记清除完成垃圾空间的回收
- 当有新生代对象晋升且老生代存储空间没法存储晋升的对象时,就会采用标记整理进行空间优化
- 采用增量标记进行效率优化
当垃圾回收进行工作的时候,会阻塞JS程序的执行,而标记增量就是把一整段的垃圾回收操作拆分成一些小部分组合着去完成回收
新老生代回收对比
- 新生代区域垃圾回收使用空间换时间,因为新生代区域较小,而且空间还一分为二,总会有一半的空间是空闲的
- 老生代区域垃圾回收不适合复制算法,因为空间比较大,用复制算法的话会有较多的空闲空间,而且老生代存储的对象数据会比较多,所以用复制算法的话会消耗较多的时间
Performance工具
内存问题的外在体现
- 页面出现延迟加载或经常性暂停(限定网络没有问题)
- 页面持续性出现糟糕的性能
- 页面的性能随时间延长越来越差
界定内存问题的标准
- 内存泄漏: 内存使用持续升高
- 内存膨胀: 在多数设备上都存在的性能问题
- 频繁垃圾回收: 通过内存变化图进行分析
事件流
事件流包含三个阶段:
事件捕捉阶段:事件开始由顶层对象触发,然后逐级向下传播,直到目标元素
处于目标阶段:处在绑定事件的元素上
事件冒泡阶段:事件由具体的元素先接收,然后逐级向上传播,直到document
事件委托
在目标元素要绑定事件时,可以在父元素绑定事件,因为事件流的规则,会向下传播直到目标元素,再根据 event.target来判断是否目标元素即可
防抖和节流
为什么需要防抖和节流:
- 在一些高频率事件触发的场景下我们不希望对应的事件处理函数多次执行
防抖
当事件快速连续不断触发时,动作只执行一次
/*
handle 最终需要执行的事件
wait 事件出发之后多久开始执行
immediate 控制执行第一次还是最后一次
*/
function myDebounce(handle, wait, immediate) {
//参数类型判断及默认值处理
if (typeof handle !== 'function') {
throw new Error('must be a function')
}
if (typeof wait !== 'number') {
imediate = wait
wait = 300
}
if (typeof immediate !== 'boolean') {
immediate = false
}
let timer = null
return function proxy(...args) {
let self = this,
init = immediate && !timer
clearTimeout(timer)
timer = setTimeout(() => {
timer = null
!immediate ? handle.call(self, ...args) : null
}, wait)
init ? handle.call(self, ...args)
}
}
节流
固定周期内,只执行一次动作,若有新事件触发,不执行.
function myThrottle(handle, wait) {
if (typeof handle !== 'function') throw new Error('handle must be a function')
if (typeof wait !== 'number') wait = 400
let previous = 0 //记录上一次执行时的时间
return function proxy(...args) {
let now = new Date()
let self = this
let interval = wait - (now - pervious)
if (interval <= 0) {
handle.call(self, ...args)
previous = now
}
}
}