接着上文闭包,我们来聊聊内存泄漏,看完下面文章,你将了解到:
1、什么是内存泄漏
2、有哪些code会导致内存泄漏
3、如何规避内存泄漏
一、探究内存泄漏之前,我们先了解下垃圾回收机制(GC)
垃圾回收机制(GC):
由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。
JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要
像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解
释器将会消耗完系统中所有可用的内存,造成系统崩溃。--《JavaScript权威指南(第四版)》
对以上简单的总结就是:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。下面就让我们一起看看垃圾收集器是如何回收内存的:
垃圾回收机制(gc)分为:标记-清除算法与引用计数垃圾收集
1、标记-清除算法最为常用 在JavaScript中,标记清除是最常用的方式. 例如:在函数中声明一个变量,就将这个变量定义为“进入环境”。从逻辑上讲 进入环境的变量其内存不能被释放,因为只要有流执行,这些变量都可能被用到。当变量离开环境后,解释器标记其为“离开环境”,此时,当垃圾收集器,在下个周期时就会回收这部分的内存.
function foo(){
let num=1;//被标记,进入环境
let count=60;//被标记,进入环境
}
foo();//函数执行完毕,num,count被标记为离开环境,被gc回收
2、引用计数垃圾收集

这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。定期的,垃圾回收器将从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和所有不能获得的对象。可以获得的对象,将不会被垃圾回收器回收,不能获得的对象将会被垃圾回收器回收,从而达到释放内存的目的. 具体实现:
function obj(){
let a={};//定义a引用对象 初始化 a引用次数为0
let b=a;//a被引用次数1
let c=a;//a被引用次数2
let b={};//因b引用被切断,此时上下文a的引用次数减一,引用次数变为1
let c=[];//同上此时a的引用次数为0,内存等待回收
}
看了上面的例子,我们再看下理论:
解释器通过记录每个值被引用的次数,来判定该值是否不再需要.
当声明了一个变量(这里为b)并将一个引用类型(function,object,array)值(这里为a),赋值给该变量(b)时,则这个值(a)的引用次数就是1.当这个变量又被赋值给另外一个变量时,此时这个值(a)的引用计数加1,一次类推.但当某一个变量,又重新被赋值别的变量(比如b,原来是引用a此时,被赋值一个对象),则原引用类型的值(a),引用此时减一,以此类推...
如上code:a的引用计数为0,对象不再被使用,等待垃圾收集器回收. 理论上,如果按照以上的原理进行coding,内存是能够及时被回收掉,有效的节省内存资源,但往往事与愿违,下面让我们一探内存泄漏
二、内存泄漏
内存泄漏(Memory Leak)顾名思义.就是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放【gc无法进行回收,比如因:闭包、全局变量的引用、循环引用、子dom的引用、以及事件、定时没有被及时的释放等都会引起内存泄漏】,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果的现象。
下面我们来看看,有哪些方面的内存泄漏:
1、首当其冲的是闭包引起的内存泄漏,最为常见
闭包可以维持函数内部变量驻留内存,使其得不到释放.
function onclick(){
let obj=document.getElementById("button");
obj.onclick=function(){
//.
}
}
上例,函数内部定义了一个变量,这个变量的事件引用了内部函数,并且这个事件回调函数的引用外暴了,形成闭包.
解决方法一: 将事件处理函数定义在函数外部,解除闭包.
//defeat out
function onClickHandeler(){
//to do something
}
function onclick(){
let obj=document.getElementById("button");
obj.onclick=onClickHandeler;
}
解决方法二: 删除对dom的引用,解除绑定
function(){
let obj=document.getElementById("button");
obj.onclick=function onClickHandeler(){
//to do something
}
obj = null;
}
2、意外的全局变量引起的内存泄漏
function(){
menu='navite';//menu为一个全局变量,全局变量常驻内存
}
3、没有清理的dom元素的引用
//dom still exist
function click(){
const button=document.getElementById('button');
butto.click();
}
// button has removed
function removeButton(){
document.body.removeChild(document.getElementById('button'));
}
4、定时器没有被及时的被销毁
var conentByName=getConentByName();
setInterval(function(){
let content=document.getElemetById('key_id');
if(content){
content.text = conentByName;
}
},1000);
如果key_id这个元素从dom元素移除,那么这个定时仍然存在.因为函数中包含对conentByName的引用,外部对象也得不到释放. 解决办法:在调用之前,对定时进行清除.
clearInterval();
var conentByName=getConentByName();
setInterval(function(){
let content=document.getElemetById('key_id');
if(!!content){
content.text = conentByName;
return;
}
clearInterval();
conentByName=null;
},1000);
5、子元素存在引起的内存泄漏
<div id='parent'>
<div>
<ul>
<li><li>
<li><li>
<li><a class='child'></a><li>
</ul>
</div>
</div>
function(){
let parentById=document.getElementById('parent');
let childByName=document.getElementByName('child');
document.body.removeChild(parentById);
}
parentById,childByName直接被JavaScript变量引用,中间dom链被间接引用.所以 虽然,parentById层dom元素被删除,但由于childByName被间接引用,导致childByName这条dom链仍然存在.都不会被删除.
解决方案: 把不需要的子元素进行遍历删除
6、监听事件造成的泄漏
this.filterWidget = new FilterWidget(this, {
categoryList: [{
title: '0',
startIndex: 0,
list: ['zjl','lisi']
}]
});
this.filterWidget.on('EVENT_FILTER', this);
上面事件,如果不及时off掉,会造成事件监听的重复注册.
解决办法:调用之前,off掉并且在页面离开时,在生命周期钩子中销毁掉
this.filterWidget.off('EVENT_FILTER', this);
7、对Promise对象,在不用的时候及时的reject掉
如下:
define(function (require) {
'use strict';
var View = require('core/View'),
return View.extend({
template: templates['page'],
onInit: function (option) {
this.providerPromise = this.Provider().done(function (html) {
// to do html
}).fail(function(){
});
},
onDestroy:function(){
this.providerPromise && this.providerPromise.reject();
this.filterWidget.on('EVENT_FILTER', this);
});
});
8、循环引用造成的泄漏,只有老的IE版本才有,目前ie9及以上已经优化了这个bug,这里就不做探讨了.
三、规避内存泄漏
1、谨慎使用闭包
a、在业务不需要用到的内部函数,可以重构在函数外,实现解除闭包.
b、闭包内,局部变量使用后或不再需要,及时的清除掉
2、减少不必要的全局变量,如果用了,最好在声明周期钩子中或再函数调用之前,及时的清除掉.
3、减少生命周期较长的对象,及时对无用的数据进行释放销毁.
4、避免创建过多的对象,对不用的对象及时的释放.
5、对注册的事件,再不用的时候,及时的解耦.释放资源.
欢迎关注,【前端突击】 猎鹰突击,迎难而上,文章会不断的完善,期待你的加入...
