整理一下「JS避免内存泄漏」,既陌生又熟悉的东西

前言 大家好,我是林三心,上一篇我给大家讲了赠你13张图,助你20分钟打败了「V8垃圾回收机制」,但是关知道回收机制是不行的,V8垃圾回收机制固然很强,但是我们也不能随便就制造很多垃圾让它回收,咱们得在开发中尽量减少垃圾的数量,今天就跟大家讲一讲如何避免JS垃圾过多,内存泄漏吧
为什么要避免 什么是内存泄漏呢?就是有些理应被回收的垃圾,却没被回收,这就造成了垃圾越积越多。
内存泄漏,听起来很遥远,但其实离我们很近很近,我们平时都直接或者间接地去接触过它。例如,有时候你的页面,用着用着就卡了起来,而且随着时间的延长,越来越卡,那这个时候,就要考虑是否是内存泄漏问题了,内存泄漏是影响用户体验的重大问题,所以平时通过正确的代码习惯去避免它,是非常有必要的。
如何监控内存状况 咱们一直强调内存内存,但是感觉他是很虚无缥缈的东西,至少也得让咱们见见它的真面目吧?
浏览器任务管理器
打开方式:在浏览器顶部右键,打开任务管理器
整理一下「JS避免内存泄漏」,既陌生又熟悉的东西
文章图片

打开后,咱们看到内存JavaScript内存(括号里)

  • 内存:页面里的原始内存,也就是DOM节点的总占用内存
  • JavaScript内存(括号里):是该页面中所有可达对象的总占用内存
那什么是可达对象呢?简单说就是:就是从初始的根对象(window或者global)的指针开始,向下搜索子节点,子节点被搜索到了,说明该子节点的引用对象可达,搜不到,说明该子节点对象不可达。举个例子:
// 可达,可以通过window.name访问 var name = '林三心'function fn () { // 不可达,访问不了 var name = '林三心' }

回到我们的任务管理,此时我们在页面中编写一段代码:

点击前:
整理一下「JS避免内存泄漏」,既陌生又熟悉的东西
文章图片

点击后,发现内存瞬间上升:
整理一下「JS避免内存泄漏」,既陌生又熟悉的东西
文章图片

Performance
使用Chrome浏览器的无痕模式,是为了避免很多其他因素,影响咱们查看内存:
整理一下「JS避免内存泄漏」,既陌生又熟悉的东西
文章图片

按F12打开调试窗口,选择Performance
整理一下「JS避免内存泄漏」,既陌生又熟悉的东西
文章图片

【整理一下「JS避免内存泄漏」,既陌生又熟悉的东西】咱们就以掘金首页为例吧!点击录制 -> 刷新掘金 -> 点击stop,可以看到以下指标随着时间的上下波动
  • JS Heap:JS堆
  • Documents: 文档
  • Nodes: DOM节点
  • Listeners: 监听器
  • GPU Memory: GPU内存
堆快照
堆快照,顾名思义,就是将当前某一个页面的堆内存拍下照片存起来,同一个页面,执行某个操作前,录制堆快照是一个样,有可能执行完后,录制的堆快照又是另外一个样。
整理一下「JS避免内存泄漏」,既陌生又熟悉的东西
文章图片

还是以掘金首页为例,可以看到当前页面内存为13.3M,咱们可以选择Statistics,查看数组,对象,字符串等所占内存

如何避免 上面说了,其实内存泄漏问题离我们很近,我们可能都直接或者间接造成过。接下来就说说如何避免这个问题吧,可能也是你开发中的坏习惯哦!
减少全局变量
我们在开发中可能遇到过这样的代码,其实我们只是想把a当做局部变量而已,但是忘记写var,let,const了:
document.getElementById('btn').onclick = function () { // a 未在外部声明过 a = new Array(1000000).fill('Sunshine_Lin') }上方代码等同于 var a document.getElementById('btn').onclick = function () { a = new Array(1000000).fill('Sunshine_Lin') }

这样有什么坏处呢?咱们前面说过可达性,在这里就可以解释了。上方代码这么写的话,咱们可以通过window.a去访问到a这个全局变量,所以a是可达的,他不会被当做垃圾去回收,这导致他会一直占用内存而得不到释放,消耗性能,违背了我们的初衷。咱们可以通过堆快照来验证一下,步骤是:录制 -> 点击按钮 -> 录制,比较两次的结果,点击后,内存大了4M,查看Statistics,发现数组内存大了很多,没得到释放:

那应该怎么改良呢?可以加上定义变量符:
document.getElementById('btn').onclick = function () { let a = new Array(1000000).fill('Sunshine_Lin') }

看看效果,由于局部变量,不可达,每执行一次函数,就会被回收,得到释放,所以不会一直占着内存,点击前后的内存是差不多的:

未清除定时器
请看这一段代码,在这段代码中,执行完fn1函数,按理说arr数组会被回收,但是他却回收不了。为什么呢?因为定时器里的a引用着arr,并且定时器不清除的话,a就不会被回收,a不回收就会一直引用着arr,那么arr肯定也回收不了了。
function fn() { let arr = new Array(1000000).fill('Sunshine_Lin') setInterval(() => { let a = arr }, 1000) } document.getElementById('btn').onclick = function () { fn() }

Performace:录制 -> 手动垃圾回收 -> 连点五次按钮 -> 手动垃圾回收 -> 结束
首尾两次手动垃圾回收,是为了对比首尾两次垃圾内存最低点,而如果没有内存泄漏问题的话,首尾两次最低点应该是相同的,这里可以看到,尾部比首部多出的那部分,就是没有被回收的内存量

上面说了,arr数组为啥没被回收?是因为定时器没清除,导致a一直引用arr,那怎么解决呢?直接把定时器清除就行了。
function fn() { let arr = new Array(1000000).fill('Sunshine_Lin') let i = 0 let timer = setInterval(() => { if (i > 5)clearInterval(timer) let a = arr i++ }, 1000) } document.getElementById('btn').onclick = function () { fn() }

再看看Performance,发现首位两次的内存量是一样的,这就说明正常了

合理使用闭包
咱们来看这一段代码:
function fn1() { let arr = new Array(100000).fill('Sunshine_Lin')return arr } let a = [] document.getElementById('btn').onclick = function () { a.push(fn1()) }

按理说,fn1执行完后,arr会被回收,但是在这段代码中,却是没有被回收,为什么呢?因为fn1执行后,将arrreturn出去,然后arrpush进a数组了,而a数组是个全局变量,a数组是不会被回收的,那么a数组里的东西自然也不会被回收,这就导致arr不会被回收,等到点击越来越多次,不可被回收的arr就会越来越多,如果a后来没有被用到,那这些arr就成无用的垃圾了,咱们可以通过Performance堆快照来验证:
Performace:录制 -> 手动垃圾回收 -> 连点五次按钮 -> 手动垃圾回收 -> 结束
首尾两次手动垃圾回收,是为了对比首尾两次垃圾内存最低点,而如果没有内存泄漏问题的话,首尾两次最低点应该是相同的,这里可以看到,尾部比首部多出的那部分,就是没有被回收的内存量

堆快照:第一次录制 -> 连点5次按钮 -> 第二次录制
会发现,点击前后,内存多了很多,多出来的就是未被回收的内存量

分离DOM
什么叫分离DOM呢?还是利用代码来说话:
let btn = document.getElementById('btn') document.body.removeChild(btn)

虽然最后把button给删除了,但是因为全局变量btn对此DOM对象引用着,导致此DOM对象一直没有被回收,这个DOM对象就称为分离DOM,咱们可以通过堆快照来验证这个问题,在堆快照里搜索detached(中文意思为:独立,分离)

这个问题很好解决,删除button后,顺便把btn设置成null就行了:
let btn = document.getElementById('btn') document.body.removeChild(btn) btn = null

此时才是真的把button这个DOM,从js中彻底抹去:

参考资料
  • 淘宝前端是怎么做优化?如何高效书写 JavaScript ?提高 JS 性能有哪些骚操作?
  • 一文带你了解如何排查内存泄漏导致的页面卡顿现象
结语 我是林三心,一个热心的前端菜鸟程序员。如果你上进,喜欢前端,想学习前端,那咱们可以交朋友,一起摸鱼哈哈,摸鱼群,加我请备注【思否】
整理一下「JS避免内存泄漏」,既陌生又熟悉的东西
文章图片

    推荐阅读