一、不正当的闭包
闭包是指有权访问另一个函数作用域中的变量的函数,通常情况闭包就是函数内部嵌套并 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 面板