一个addEvent引发的内存泄露

  前段时间群里的朋友发了一个他自己改的addEvent,相比Dean Edwards 2005写的那个addEvent稍有变化(我只看过这个版本的,其他主流框架的还没时间研究过),但主要思想还是Dean Edwards开创的数据缓存技术,毕竟技术都是大师们开创的,我们一直都只是模仿而已。以下是我加过注释的代码:

 

代码
var cache =
{
//计数器
guid : 1, fuid : 1,
//创建监听?
createListener : function(guid)
{
//创建一个闭包
return function()
{
//事件执行,并指向对象
handle.apply( cache[guid].elem, arguments );
};
}
}

//添加事件
var addEvent = function( elem, type, handler )
{
//elem-元素,type-事件类型,事件
//为元素创建一个唯一的指定的标记,如果当前元素还未添加标记则添加,否则使用已有的
var guid = elem.guid || (elem.guid = cache.guid++);
//创建当前元素对应的标记对象
if( !cache[guid] )

cache[guid]
= {
//当前标记对象对应的元素
elem : elem,
//为当前元素建立一个监听
listener : cache.createListener(guid),
//当前对象对应的执行函数列表
events : {}
};
//如果当前传入的事件类型还不存在于当前列表中
if( type && !cache[guid].events[type] )
{
//为当前的事件列表创建一个字面量
cache[guid].events[type] = {};
//如果是标准浏览器
if( elem.addEventListener )
elem.addEventListener( type, cache[guid].listener,
false );
else if( elem.attachEvent )
elem.attachEvent(
'on' + type, cache[guid].listener );
}
//当前事件
if( handler )
{
//为当前事件添加一个标记
if( !handler.fuid )
//对应着有序的全局标记
handler.fuid = cache.fuid++
//为对应的事件类型添加事件
cache[guid].events[type][handler.fuid] = handler;
}
//断开对事件的引用
elem = null;
}

//移除事件
var removeEvent = function( elem, type, handler )
{
//找到当前元素对应的对象储存列表
var handle = cache[elem.guid], events, ret;

//如果存在
if( handle )
{
events
= handle.events;
//如果没有指定要删除的类型,就删除所有的
if( type === undefined )
for( var type in events )
removeEvent( elem, type );
else
{
//如果指定类型了
if( events[type] )
{
//且事件存在
if( handler )
//移除当前对应标记的事件
delete events[type][handler.fuid];
else
//如果不存在就移除当前事件下所有要执行的函数
for( var func in events[type] )
delete events[type][func];

for( ret in events[type] ) break;
//如果当前元素的事件执行列表已被清空
if( !ret )
{
//移除对应事件的的包装函数
if( elem.removeEventListener )
elem.removeEventListener( type, handle.listener,
false);
else if( elem.detachEvent )
elem.detachEvent(
'on' + type, handle.listener );

ret
= null;
//移除当前事件列表中对应的事件
delete events[type];
}
}
}
//如果当前元素没有绑定过任何事件类型的事件
for( ret in events ) break;
if( !ret )
//移除全局对应的标记
delete cache[elem.guid];
}
}

var handle
= function(event)
{
event = arguments[0] = fix( event || window.event );

var handlers
= cache[this.guid].events[event.type];

for( var i in handlers )
{
this.func = handlers[i];
if( this.func(event) === false )
event.preventDefault();
}
}

var fix
= function(event)
{
if( !event.preventDefault )
event.preventDefault = function(){ this.returnValue = false; };

if( !event.stopPropagation )
event.stopPropagation = function(){ this.cancelBubble = true; };

if( !event.target )
event.target = event.srcElement || document;

// check if target is a textnode (safari)
if( event.target.nodeType == 3 )
event.target = event.target.parentNode;

if( !event.relatedTarget && event.fromElement )
event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;

return event;
}

  这个相对2005 Dean Edwards的版本,数据添加部分变化不大,就是把数据集中到一个对象当中,还有就是为每一个触发事件时要执行的函数或方法增加了一个计数器,易于管理,主要的变动应该就是移除事件的那部分了,经过多次的判断,把没必要保留的缓存数据删除掉了,也算是对资源的一种优化吧。

  本身这个方法那个童鞋改的不错,但最终引起我注意的是他发现了这个方法存在一处内存泄露,并最终将出现问题的代码确定为这个位置

 

代码
var fix = function(event)
{
if( !event.preventDefault )
event.preventDefault = function(){ this.returnValue = false; };

if( !event.stopPropagation )
event.stopPropagation = function(){ this.cancelBubble = true; };

if( !event.target )
event.target = event.srcElement || document;

// check if target is a textnode (safari)
if( event.target.nodeType == 3 )
event.target = event.target.parentNode;

if( !event.relatedTarget && event.fromElement )
event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;

return event;
}

event.preventDefault = function(){ this.returnValue = false; };

event.stopPropagation = function(){ this.cancelBubble = true; };

这两处。我回过头看了看Dean Edwards的源码,他也特意将这两个匿名函数写到了外层作用域,避免了内部匿名函数造成的闭包。

经过测试我发现,把代码分别删除一部分的两种情况都不会造成泄露。

 

代码
var fix = function(event)
{
if( !event.preventDefault )
event.preventDefault = function(){ this.returnValue = false; };

if( !event.stopPropagation )
event.stopPropagation = function(){ this.cancelBubble = true; };

return event;
}

 

代码
var fix = function(event)
{
if( !event.target )
event.target = event.srcElement || document;

// check if target is a textnode (safari)
if( event.target.nodeType == 3 )
event.target = event.target.parentNode;

if( !event.relatedTarget && event.fromElement )
event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;

return event;
}

  也就是说只有完整的代码才符合产生内存泄露的条件。那么关键点就集中到了对event添加匿名函数方法和event.srcElement上面了。event.srcElement定义为引起事件的元素,但通常情况下,这个属性都引用了绑定事件的元素。这样的话,经过分析,我个人得出以下结论。元素的事件触发的时候执行handle方法,而handle方法把event对象传递给了fix方法,并在内部实现了对event对象的扩展,最终返回了event,而这个时候event扩展出来的方法中出现了一个匿名函数,导致fix方法在执行后仍无法被回收,而且通过event.srcElement,使fix方法内部出现了对触发事件元素的引用(大部分情况下)。而handle方法又始终被元素引用着,这样应该就造成了循环引用。导致跨页面的内存泄露。

  可能造成泄漏的原因,远远比我分析的更复杂,毕竟ie可能造成的内存泄漏太多样了,但还是希望说出我的想法,以便今后进一步推敲。

posted @ 2009-12-21 00:50  黄金小强  阅读(463)  评论(0编辑  收藏  举报