内存泄露
前言
随着SPA(单页面应用程序)的兴起,促使我们更加关注与内存相关的JavaScript编码实践。如果应用使用的内存越来越多,就会影响性能,甚至导致浏览器的崩溃。本文就来看看JavaScript中常见的内存泄漏以及如何避免内存泄露
一、什么是内存泄露?
JavaScript就是所谓的垃圾回收语言之一,垃圾回收语言通过定期检查哪些先前分配的内存仍然可以从应用程序的其他部分“访问” 来帮助开发人员管理内存。垃圾回收语言中泄露的主要原因是不需要的引用。如果你的JavaScript 应用程序经常发生崩溃、高延迟和性能差,那么一个潜在的原因可能就是内存泄露
在JavaScript中,内存泄露是有生命周期的:
- 分配内存:内存由操作系统分配,允许程序使用它。在JavaScript中,分配内存是自动完成的
- 使用内存:这是程序实际使用先前分配的内存的空间。当代码中使用分配变量时,会发生读取和写入操作
- 释放内存:释放不需要的内存,这样就会空闲并可以再次利用。在JavaScript中,释放内存是自动完成的
在JavaScript中,对象会保护在堆内存中,可以根据引用链从根访问它们。垃圾收集器是JavaScript引擎中的一个后台进程,用于识别无法访问的对象、删除它们并回收内存
下面是垃圾收集器根到对象的引用链示例:
当内存中应该在垃圾回收周期中清理的对象,通过一个对象的无意引用从根保持可访问时,就会发生内存泄漏。蒋冗余对象保留在内存中会导致应用程序内部使用过多的内存,并可能导致性能下降
那该如何判断代码正在泄露内存呢?通常内存泄露是很难被发现的,并且浏览器在运行它时不会抛出任何错误。如果注意到页面的性能越来越差,浏览器的内置工具可以帮助我们确定是否存在内存泄漏以及导致内存泄漏的对象
二、常见的内存泄漏
我们可以通过了解在JavaScript中如何创建不需要的引用来防止内存泄漏。以下情况就会导致不需要的引用
1.意外全局变量
全局变量始终可以从全局对象(在浏览器中,全局对象是window)中获得,并且永远不会被垃圾回收。在非严格模式下,以下行为会导致变量从局部范围泄露到全局
(1) 未声明的变量赋值
这里我们给函数中一个未声明的变量str赋值,这时str便会成为一个全局变量
function myName (){
str="gov"
}
//等价于=>
function myName (){
window.str="gov"
}
这样就会创建一个多余的全局变量,当执行完myName函数之后,变量str仍然会存在于全局对象中:
myName()
window.str // gov
(2) 使用指向全局对象的this
使用以下方式也会创建一个意外的全局变量:
function myName(){
this.name="gov";
}
myName();
这里myName实在全局对象中调用的,所以其this是指向全局对象的 (这里是window)
window.name //gov
我们可以通过使用严格模式("use strict") 来避免一切。在JavaScript文件的开头,它将开启更严格的JavaScript解析模式,从而防止意外的创建全局变量
需要特别注意那些临时存储和处理大量信息的全局信息。如果必须使用全局变量存储数据,就使用全局变量储存数据,但在不再使用时,就手动将其设置为null,或者在处理完后重新分配。否则的话,请尽可能的使用局部变量
2.计时器
使用 setTimeout 或setInterval 引用回调中的某个对象是防止对象被垃圾收集的最常见方法。如果我们在代码中设置了循环计时器,只要回调是可调用的,计时器回调中对对象的引用就会保持活动状态
在下面的示例中,只有在清除计时器后,才能对数据进行垃圾收集,由于我们没有对setInterval的引用,所以它永远无法被清除和删除数据,hugeString会一直保存在内存中,直到应用程序停止,尽管从未使用过
function setCallback(){
const data ={
total:0,
hugeString:new Array(100000).join("x")
}
return function cb(){
data.total++;
console.log(data.total)
}
}
setInterval(setCallback(),1000);
当执行上面代码会每秒输出一个数字;那我们应该如何去阻止他呢?尤其是在回调的寿命未定义或不确定的情况下:
- 修改计时器回调中引用的对象;
- 必要时使用从计时器返回的句柄(定时器的标识符) 取消它
3.闭包
我们都知道,函数范围内的变量在函数退出调用堆栈后,如果函数外部没有任何指向它们的引用,则会被清除。尽管函数已经完成执行,其执行上下文和变量环境早已消失,但闭包蒋保持变量的引用和活动状态
function outer(){
const potentiallyHugeArray=[];
return function inner(){
potentiallyHugeArray.push("gov");
console.log("gov")
}
}
const sayName=outer();
function repeat(fn,num){
for(let i = 0; i<num;i++){
fn();
}
}
repeat(sayName,10)
很明显,这里就形成了一个闭包,potentiallyHugeArray永远不会从任何函数返回,也无法访问,但它的大小可能会无限增长,这取决于调用函数inner()的次数
那该如何防止这个问题呢?闭包是不可避免的,也是JavaScript不可或缺的一部分,因此重要的是:
- 了解何时创建闭包以及闭包保留了哪些对象
- 了解闭包的预期寿命和用法(尤其是用作回调时)
4.事件监听
活动事件侦听器蒋防止在其范围内捕获的所有变量被垃圾收集。添加后,事件侦听器蒋一直有效,直到:
- 使用removeEventListener()显示删除
- 关联的DOM元素被移除
对于某些类型的事件,它会一直保留到用户离开页面,就像多次单击的按钮一样。但是,有时我们希望事件侦听器执行一定次数,就要使用addEventListener的第三个参数 once:true 来实现这个侦听器监听一次就自动删除
5.缓存
如果我们不断地将内存添加到缓存中,而不删除未使用的对象,并且没有一些限制大小的逻辑,那么缓存可以无限增长,如果我们需要将那些永远不会被重用的变量从缓存中清除,可以使用WeakMap来解决此问题,它是一种具有弱键引用的数据结构,仅接受对象作为键·。如果我们使用一个对象作为键,并且它是对该对象的唯一引用————相关变量将从缓存中删除并被垃圾收集
6.分离的DOM元素
如果DOM节点具有来自JavaScript的直接引用,它将防止对其进行垃圾收集,即时从DOM树中删除该节点之后也是如此
在下面的示例中,创建了一个div元素并将其附加到document.body中。removeChild()就无法按预期工作,堆快照将显示分离的HTMLDicElement,因为仍有一个变量指向div
function createElement(){
const div = document.createElement("div")
div.id="detached";
return div;
}
//即使在调用deleteElement()之后,它仍将继续引用DOM元素
const detachedDiv = createElement();
document.body.appendChild(detachedDiv);
function deleteElement(){
document.body.removeChild(document.getElementById("detached"));
}
deleteElement();
要解决此问题,可以将DOM引用移动到本地范围。在下面的示例中,在函数appendElement()完成后,将删除指向DOM元素的变量
function createElement(){...}
//DOM引用在函数范围内
function appendElement(){
const detachedDiv = createElement();
document.body.appendChild(detachedDiv);
}
appendElement();
function deleteElement(){
document.body.removeChild(document.getElementById("detached"))
}
deleteElement();
三、识别内存泄露
调试内存问题是一项复杂的工作,我们可以使用Chrome DevTools 来识别内存图和一些内存泄露,我们需要关注两个方面:
- 使用性能分析器可视化内存消耗
- 识别分离的DOM节点
转载自:https://juejin.cn/post/7267737577613557800