js哪些操作会内存泄漏JavaScript 中内存泄漏的原因以及对策




js哪些操作会内存泄漏JavaScript 中内存泄漏的原因以及对策

2022-07-21 2:26:29 网络知识 官方管理员

相比过去的网页,今天流行的SPA需要开发人员更加关注程序中的内存泄漏情况。因为以前的网站在浏览时会不断刷新页面,可是SPA网站往往只有少数几个页面,很少完全重新加载。这篇文章主要探讨JS代码中容易导致内存泄漏的模式,并给出改进对策。

什么是内存泄漏?

内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

JavaScript是一个有垃圾回收机制的语言,我们不需要手动回收内存。当本应在垃圾回收周期中清理的内存中的对象,通过另一个对象的无意引用从根保持可访问状态时,就会发生内存泄漏,并可能导致性能下降的后果。

内存泄漏通常很难发现和定位。泄漏的JavaScript代码从任何层面都不会被认为是无效的,并且浏览器在运行时不会引发任何错误。

检查内存使用情况的最快方法是查看浏览器的任务管理器(不是操作系统的那个任务管理器)。在Linux和Windows上按Shift+Esc来访问Chrome的任务管理器;Firefox则在地址栏中键入about:performance。我们能用它查看每个选项卡的JavaScript内存占用量。如果发现异常的内存使用量持续增长,就很可能出现了泄漏。

开发工具提供了更高级的内存管理方法。通过Chrome的性能工具,我们可以直观地分析页面在运行时的性能。像下面这种模式就是内存泄漏的典型表现:

js哪些操作会内存泄漏(JavaScript中内存泄漏的原因以及对策)(1)

除此之外,Chrome和Firefox的开发工具都可以用“内存”工具进一步探索内存使用情况。

JS代码中常见的几个内存泄漏源

全局变量

全局变量总是从根可用,并且永远不会回收垃圾。在非严格模式下,一些错误会导致变量从本地域泄漏到全局域:

  • 将值分配给未声明的变量;
  • 使用“this”指向全局对象。
functioncreateGlobalVariables(){leaking1='Ileakintotheglobalscope';//assigningvaluetotheundeclaredvariablethis.leaking2='Ialsoleakintotheglobalscope';//'this'pointstotheglobalobject};createGlobalVariables();window.leaking1;//'Ileakintotheglobalscope'window.leaking2;//'Ialsoleakintotheglobalscope'

预防措施:使用严格模式("usestrict")。

闭包

函数作用域内的变量将在函数退出调用栈后清除,并且如果函数外部没有其他指向它们的引用,则将清理它们。但闭包将保留引用的变量并保持活动状态。

functionouter(){constpotentiallyHugeArray=[];returnfunctioninner(){potentiallyHugeArray.push('Hello');//functioninnerisclosedoverthepotentiallyHugeArrayvariableconsole.log('Hello');};};constsayHello=outer();//containsdefinitionofthefunctioninnerfunctionrepeat(fn,num){for(leti=0;i<num;i++){fn();}}repeat(sayHello,10);//eachsayHellocallpushesanother'Hello'tothepotentiallyHugeArray//nowimaginerepeat(sayHello,100000)

在此示例中,从任何一个函数都不会返回potentialHugeArray,并且无法到达它,但它的大小可以无限增加,具体取决于我们调用函数inner()的次数。

预防措施:闭包是肯定会用到的,所以重要的是:

  • 了解何时创建了闭包,以及它保留了哪些对象;
  • 了解闭包的预期寿命和用法(尤其是用作回调时)。

计时器

如果我们在代码中设置了递归计时器(recurringtimer),则只要回调可调用,计时器回调中对该对象的引用就将保持活动状态。

在下面的示例中,由于我们没有对setInterval的引用,因此它永远不会被清除,并且data.hugeString会一直保留在内存中。

functionsetCallback(){constdata={counter:0,hugeString:newArray(100000).join('x')};returnfunctioncb(){data.counter++;//dataobjectisnowpartofthecallback'sscopeconsole.log(data.counter);}}setInterval(setCallback(),1000);//howdowestopit?

预防措施:尤其是在回调的生命周期不确定或undefined的情况下:

  • 了解从计时器的回调中引用了哪些对象;
  • 使用计时器返回的句柄在必要时取消它。
functionsetCallback(){//'unpacking'thedataobjectletcounter=0;consthugeString=newArray(100000).join('x');//getsremovedwhenthesetCallbackreturnsreturnfunctioncb(){counter++;//onlycounterispartofthecallback'sscopeconsole.log(counter);}}consttimerId=setInterval(setCallback(),1000);//savingtheintervalID//doingsomething...clearInterval(timerId);//stoppingthetimeri.e.ifbuttonpressed

事件侦听器

添加后,事件侦听器将一直保持有效,直到:

  • 使用removeEventListener()显式删除它;
  • 关联的DOM元素被移除。

对于某些类型的事件,应该一直保留到用户离开页面为止。但是,有时我们希望事件侦听器执行特定的次数。

consthugeString=newArray(100000).join('x');document.addEventListener('keyup',function(){//anonymousinlinefunction-can'tremoveitdoSomething(hugeString);//hugeStringisnowforeverkeptinthecallback'sscope});

在上面的示例中,用一个匿名内联函数作为事件侦听器,这意味着无法使用removeEventListener()将其删除。同样,该文档也无法删除,因此即使我们只需要触发它一次,它和它域中的内容就都删不掉了。

预防措施:我们应该始终创建指向事件侦听器的引用并将其传递给removeEventListener(),来注销不再需要的事件侦听器。

functionlistener(){doSomething(hugeString);}document.addEventListener('keyup',listener);//namedfunctioncanbereferencedhere...document.removeEventListener('keyup',listener);//...andhere

如果事件侦听器仅执行一次,则addEventListener()可以使用第三个参数。假设{once:true}作为第三个参数传递给addEventListener(),则在处理一次事件后,将自动删除侦听器函数。

document.addEventListener('keyup',functionlistener(){doSomething(hugeString);},{once:true});//listenerwillberemovedafterrunningonce

缓存

如果我们不删除未使用的对象且不控制对象大小,那么缓存就会失控。

letuser_1={name:"Peter",id:12345};letuser_2={name:"Mark",id:54321};constmapCache=newMap();functioncache(obj){if(!mapCache.has(obj)){constvalue=`${obj.name}hasanidof${obj.id}`;mapCache.set(obj,value);return[value,'computed'];}return[mapCache.get(obj),'cached'];}cache(user_1);//['Peterhasanidof12345','computed']cache(user_1);//['Peterhasanidof12345','cached']cache(user_2);//['Markhasanidof54321','computed']console.log(mapCache);//((…)=>"Peterhasanidof12345",(…)=>"Markhasanidof54321")user_1=null;//removingtheinactiveuser//GarbageCollectorconsole.log(mapCache);//((…)=>"Peterhasanidof12345",(…)=>"Markhasanidof54321")//firstentryisstillincache

在上面的示例中,缓存仍保留在user_1对象上。因此,我们还需要清除不会再重用的条目的缓存。

可能的解决方案:我们可以使用WeakMap。它的数据结构中,键名是对象的弱引用,它仅接受对象作为键名,所以其对应的对象可能会被自动回收。当对象被回收后,WeakMap自动移除对应的键值对。在以下示例中,在使user_1对象为空后,下一次垃圾回收后关联的条目会自动从WeakMap中删除。

letuser_1={name:"Peter",id:12345};letuser_2={name:"Mark",id:54321};constweakMapCache=newWeakMap();functioncache(obj){//...sameasabove,butwithweakMapCachereturn[weakMapCache.get(obj),'cached'];}cache(user_1);//['Peterhasanidof12345','computed']cache(user_2);//['Markhasanidof54321','computed']console.log(weakMapCache);//((…)=>"Peterhasanidof12345",(…)=>"Markhasanidof54321"}user_1=null;//removingtheinactiveuser//GarbageCollectorconsole.log(weakMapCache);//((…)=>"Markhasanidof54321")-firstentrygetsgarbagecollected

分离的DOM元素

如果DOM节点具有来自JavaScript的直接引用,则即使从DOM树中删除了该节点,也不会对其垃圾回收。

在以下示例中,我们创建了一个div元素并将其附加到document.body。removeChild()无法正常工作,并且由于仍然存在指向div的变量,所以堆快照将显示分离的HTMLDivElement。

functioncreateElement(){constdiv=document.createElement('div');div.id='detached';returndiv;}//thiswillkeepreferencingtheDOMelementevenafterdeleteElement()iscalledconstdetachedDiv=createElement();document.body.appendChild(detachedDiv);functiondeleteElement(){document.body.removeChild(document.getElementById('detached'));}deleteElement();//Heapsnapshotwillshowdetacheddiv#detached

怎么预防呢?一种方案是将DOM引用移入本地域。在下面的示例中,在函数appendElement()完成之后,将删除指向DOM元素的变量。

functionouter(){constpotentiallyHugeArray=[];returnfunctioninner(){potentiallyHugeArray.push('Hello');//functioninnerisclosedoverthepotentiallyHugeArrayvariableconsole.log('Hello');};};constsayHello=outer();//containsdefinitionofthefunctioninnerfunctionrepeat(fn,num){for(leti=0;i<num;i++){fn();}}repeat(sayHello,10);//eachsayHellocallpushesanother'Hello'tothepotentiallyHugeArray//nowimaginerepeat(sayHello,100000)0


发表评论:

最近发表
网站分类
标签列表