JS 程序中可能存在的内存泄漏

一、不正当的闭包

闭包是指有权访问另一个函数作用域中的变量的函数,通常情况闭包就是函数内部嵌套并 return 一个函数。

function fn1(){
  let test = 'isboyjc'
  return function(){
    console.log('hahaha')
  }
}
let fn1Child = fn1()
fn1Child()

上面是是一个典型闭包,但是它并没有造成内存泄漏,再看下面:

function fn2(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log(test)
    return test
  }
}
let fn2Child = fn2
fn2Child()

因为 return 的函数中存在函数 fn2 中的 test 变量引用,所以 test 并不会被回收,也就造成了内存泄漏。
解决办法是在函数调用后,把外部的引用关系置空:

function fn2(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log(test)
    return test
  }
}
let fn2Child = fn2
fn2Child()
fn2Child = null

二、隐式全局变量

全局变量除非被取消或者重新分配之外也是无法回收的,这也就需要我们额外的关注:

function fn(){
  // 没有声明从而制造了隐式全局变量test1
  test1 = 'isboyjc1'
  
  // 函数内部this指向window,制造了隐式全局变量test2
  this.test2 = 'isboyjc2'
}
fn()

调用函数 fn ,因为 没有声明 和 函数中this 的问题造成了两个额外的隐式全局变量,这两个变量不会被回收,这种情况我们要尽可能的避免。
除此之外,在使用全局变量做持续存储大量数据的缓存时,我们一定要记得设置存储上限并及时清理(在使用完将其置为 null 即可),不然的话数据量越来越大,内存压力也会随之增高。

var test = new Array(10000)

// do something

test = null

三、游离DOM引用

考虑到性能或代码简洁方面,我们代码中进行 DOM 时会使用变量缓存 DOM 节点的引用,但移除节点的时候,我们应该同步释放缓存的引用,否则游离的子树无法释放。

<div id="root">
  <ul id="ul">
    <li></li>
    <li></li>
    <li id="li3"></li>
    <li></li>
  </ul>
</div>
<script>
  let root = document.querySelector('#root')
  let ul = document.querySelector('#ul')
  let li3 = document.querySelector('#li3')
  
  // 由于ul变量存在,整个ul及其子元素都不能GC
  root.removeChild(ul)
  
  // 虽置空了ul变量,但由于li3变量引用ul的子节点,所以ul元素依然不能被GC
  ul = null
  
  // 已无变量引用,此时可以GC
  li3 = null
</script>

如上所示,当我们使用变量缓存 DOM 节点引用后删除了节点,如果不将缓存引用的变量置空,依然进行不了 GC,也就会出现内存泄漏。
假如我们将父节点置空,但是被删除的父节点其子节点引用也缓存在变量里,那么就会导致整个父 DOM 节点树下整个游离节点树均无法清理,还是会出现内存泄漏,解决办法就是将引用子节点的变量也置空。

四、遗忘的定时器

// 获取数据
let someResource = getData()
setInterval(() => {
  const node = document.getElementById('Node')
	if(node) {
    node.innerHTML = JSON.stringify(someResource))
	}
}, 1000)

代码中每隔一秒就将得到的数据放入到 Node 节点中去,但是在 setInterval 没有结束前,回调函数里的变量以及回调函数本身都无法被回收。
什么才叫结束呢?也就是调用了 clearInterval。如果没有被 clear 掉的话,就会造成内存泄漏。不仅如此,如果回调函数没有被回收,那么回调函数内依赖的变量也没法被回收。所以在上例中,someResource 就没法被回收。
同样,setTiemout 也会有同样的问题,所以,当不需要 interval 或者 timeout 时,最好调用 clearInterval 或者 clearTimeout来清除,另外,浏览器中的 requestAnimationFrame 也存在这个问题,我们需要在不需要的时候用 cancelAnimationFrame API 来取消使用。

五、遗忘的事件监听器

当事件监听器在组件内挂载相关的事件处理函数,而在组件销毁时不主动将其清除时,其中引用的变量或者函数都被认为是需要的而不会进行回收,如果内部引用的变量存储了大量数据,可能会引起页面占用内存过高,这样就造成意外的内存泄漏。

我们就拿 Vue 组件来举例子,React 里也是一样的:

<template>
  <div></div>
</template>

<script>
export default {
  created() {
    window.addEventListener("resize", this.doSomething)
  },
  beforeDestroy(){
    window.removeEventListener("resize", this.doSomething)
  },
  methods: {
    doSomething() {
      // do something
    }
  }
}
</script>

六、排查问题

Chrome 的开发者工具找到 Performance 这一面板,之前叫 Timeline ,它是 Chrome Devtool 用来监控性能指标的一个利器,可以记录并分析在网站的生命周期内所发生的各类事件,我们就可以通过它监控我们程序中的各种性能情况并分析,其中就包括内存。

Chrome Devtool 还为我们提供了 Memory 面板,它可以为我们提供更多详细信息,比如记录 JS CPU 执行时间细节、显示 JS 对象和相关的DOM节点的内存消耗、记录内存的分配细节等。
其中的 Heap Profiling 可以记录当前的堆内存 heap 的快照,并生成对象的描述文件,该描述文件给出了当下 JS 运行所用的所有对象,以及这些对象所占用的内存大小、引用的层级关系等等,用它就可以定位出引起问题的具体原因以及位置。

注意,可不是 Performance 面板下那个 Memory ,而是与 Performance 面板同级的 Memory 面板

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值