4

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

 2 years ago
source link: https://segmentfault.com/a/1190000041252211
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

大家好,我是林三心,上一篇我给大家讲了赠你13张图,助你20分钟打败了「V8垃圾回收机制」,但是关知道回收机制是不行的,V8垃圾回收机制固然很强,但是我们也不能随便就制造很多垃圾让它回收,咱们得在开发中尽量减少垃圾的数量,今天就跟大家讲一讲如何避免JS垃圾过多,内存泄漏

为什么要避免

什么是内存泄漏呢?就是有些理应被回收的垃圾,却没被回收,这就造成了垃圾越积越多。

内存泄漏,听起来很遥远,但其实离我们很近很近,我们平时都直接或者间接地去接触过它。例如,有时候你的页面,用着用着就卡了起来,而且随着时间的延长,越来越卡,那这个时候,就要考虑是否是内存泄漏问题了,内存泄漏是影响用户体验的重大问题,所以平时通过正确的代码习惯去避免它,是非常有必要的。

如何监控内存状况

咱们一直强调内存内存,但是感觉他是很虚无缥缈的东西,至少也得让咱们见见它的真面目吧?

浏览器任务管理器

打开方式:在浏览器顶部右键,打开任务管理器

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

  • 内存:页面里的原始内存,也就是DOM节点的总占用内存
  • JavaScript内存(括号里):是该页面中所有可达对象的总占用内存

那什么是可达对象呢?简单说就是:就是从初始的根对象(window或者global)的指针开始,向下搜索子节点,子节点被搜索到了,说明该子节点的引用对象可达,搜不到,说明该子节点对象不可达。举个例子:

// 可达,可以通过window.name访问
var name = '林三心'

function fn () {
    // 不可达,访问不了
    var name = '林三心'
}

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

<button id="btn">点击</button>
<script>
    document.getElementById('btn').onclick = function () {
        list = new Array(1000000)
    }
</script>

截屏2021-08-03 下午10.16.50.png

点击后,发现内存瞬间上升:

截屏2021-08-03 下午10.17.18.png

Performance

使用Chrome浏览器的无痕模式,是为了避免很多其他因素,影响咱们查看内存:

截屏2021-08-03 下午10.39.58.png

按F12打开调试窗口,选择Performance

image.png

咱们就以掘金首页为例吧!点击录制 -> 刷新掘金 -> 点击stop,可以看到以下指标随着时间的上下波动

  • JS Heap:JS堆
  • Documents: 文档
  • Nodes: DOM节点
  • Listeners: 监听器
  • GPU Memory: GPU内存
    juejinperf.gif

堆快照,顾名思义,就是将当前某一个页面的堆内存拍下照片存起来,同一个页面,执行某个操作前,录制堆快照是一个样,有可能执行完后,录制的堆快照又是另外一个样。

image.png

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

掘金堆快照.gif

上面说了,其实内存泄漏问题离我们很近,我们可能都直接或者间接造成过。接下来就说说如何避免这个问题吧,可能也是你开发中的坏习惯哦!

减少全局变量

我们在开发中可能遇到过这样的代码,其实我们只是想把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,发现数组内存大了很多,没得到释放:

全局变量堆快照.gif

那应该怎么改良呢?可以加上定义变量符:

document.getElementById('btn').onclick = function () {
     let a = new Array(1000000).fill('Sunshine_Lin')
}

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

局部变量堆快照.gif

未清除定时器

请看这一段代码,在这段代码中,执行完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:录制 -> 手动垃圾回收 -> 连点五次按钮 -> 手动垃圾回收 -> 结束

首尾两次手动垃圾回收,是为了对比首尾两次垃圾内存最低点,而如果没有内存泄漏问题的话,首尾两次最低点应该是相同的,这里可以看到,尾部比首部多出的那部分,就是没有被回收的内存量
定时器perf.gif

上面说了,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,发现首位两次的内存量是一样的,这就说明正常了

清除定时器perf.gif

合理使用闭包

咱们来看这一段代码:

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:录制 -> 手动垃圾回收 -> 连点五次按钮 -> 手动垃圾回收 -> 结束

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

闭包perfo.gif

堆快照:第一次录制 -> 连点5次按钮 -> 第二次录制

会发现,点击前后,内存多了很多,多出来的就是未被回收的内存量

闭包堆快照.gif

分离DOM

什么叫分离DOM呢?还是利用代码来说话:

<button id="btn">点击</button>

let btn = document.getElementById('btn')
document.body.removeChild(btn)

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

分离DOM堆快照.gif

这个问题很好解决,删除button后,顺便把btn设置成null就行了:

<button id="btn">点击</button>

let btn = document.getElementById('btn')
document.body.removeChild(btn)
btn = null

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

分离domnull.gif

我是林三心,一个热心的前端菜鸟程序员。如果你上进,喜欢前端,想学习前端,那咱们可以交朋友,一起摸鱼哈哈,摸鱼群,加我请备注【思否】

image.png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK