本文深入探讨JavaScript中实现跨浏览器通用事件绑定函数的关键要求,包括支持多重监听、忽略重复注册、this指向及执行顺序等。通过对比John Resig与Dean Edward的经典实现,分析其在不同浏览器下的表现差异,并提供一个完全满足五大核心需求的改进版方案,帮助开发者构建稳定且兼容的交互逻辑。
通用事件绑定的核心需求
在JavaScript开发中,事件绑定通常涉及三种常见方式:传统绑定、W3C标准绑定以及IE专属绑定。然而,在实际工程项目中,开发者更倾向于使用一种能够兼容所有浏览器的通用绑定方案。网络上存在多种实现思路,其中以下几种较为知名且被广泛讨论。
在深入探讨具体实现之前,我们需要明确一个优秀的addEvent()函数应当具备的核心标准。这些标准旨在确保事件处理的健壮性与一致性。
首先,该函数必须支持为同一元素的同一个事件句柄绑定多个监听函数,以允许多个逻辑同时响应同一事件。
其次,若在同一元素的同一事件句柄上多次注册完全相同的函数,系统应忽略后续注册,仅保留第一次注册的效果,避免冗余执行。
第三,监听函数内部执行的this关键字,必须准确指向当前正在处理事件的DOM节点,即触发事件句柄的那个元素。
第四,监听函数的执行顺序必须严格遵循绑定的先后顺序,确保逻辑处理的时序可控。
第五,在监听函数内部,开发者无需再使用event = event || window.event这样的代码来标准化Event对象,函数应自动处理此兼容性问题。
John Resig实现方案分析
接下来,我们分析John Resig编写的addEvent()函数实现。
function addEvent( obj, type, fn ) {
if ( obj.attachEvent ) {
obj['e'+type+fn] = fn;
obj[type+fn] = function(){obj['e'+type+fn]( window.event );}
obj.attachEvent( 'on'+type, obj[type+fn] );
} else
obj.addEventListener( type, fn, false );
}
function removeEvent( obj, type, fn ) {
if ( obj.detachEvent ) {
obj.detachEvent( 'on'+type, obj[type+fn] );
obj[type+fn] = null;
} else
obj.removeEventListener( type, fn, false );
}
该实现代码简洁直观,令人印象深刻。我们将其与前述的五点核心要求进行逐一比对,以评估其适用性。
针对第一点要求,该函数通过判断浏览器支持情况分别处理,成功实现了同一元素绑定多个监听函数的功能。
对于第三点和第五点,由于分别调用了addEventListener和attachEvent,它们天然支持this指向当前节点,且内部已处理Event对象的标准化,因此这两点也得到满足。
然而,第二点要求并未完全满足。原因在于,W3C标准的addEventListener会自动忽略重复注册,但IE的attachEvent却允许重复注册,导致行为不一致。
此外,第四点要求也未得到保证。由于DOM标准并未明确规定调用对象时间处理函数的具体顺序,因此不能假设它们会严格按照注册顺序执行,这在某些浏览器中可能导致时序问题。
尽管存在上述局限,该函数在大多数场景下仍是一个优秀且实用的解决方案。
Dean Edward实现方案分析
随后,我们考察Dean Edward编写的addEvent()函数实现。
function addEvent(element, type, handler) {
if (!handler.$$guid) handler.$$guid = addEvent.guid++;
if (!element.events) element.events = {};
var handlers = element.events[type];
if (!handlers) {
handlers = element.events[type] = {};
if (element["on" + type]) {
handlers[0] = element["on" + type];
}
}
handlers[handler.$$guid] = handler;
element["on" + type] = handleEvent;
}
addEvent.guid = 1;
function removeEvent(element, type, handler) {
if (element.events && element.events[type]) {
delete element.events[type][handler.$$guid];
}
}
function handleEvent(event) {
var returnValue = true;
event = event || fixEvent(window.event);
var handlers = this.events[event.type];
for (var i in handlers) {
this.$$handleEvent = handlers[i];
if (this.$$handleEvent(event) === false) {
returnValue = false;
}
}
return returnValue;
};
function fixEvent(event) {
event.preventDefault = fixEvent.preventDefault;
event.stopPropagation = fixEvent.stopPropagation;
return event;
};
fixEvent.preventDefault = function() {
this.returnValue = false;
};
fixEvent.stopPropagation = function() {
this.cancelBubble = true;
};
该函数基于传统绑定方法构建,因此能够在所有主流浏览器中正常运行,且不会引发内存泄漏问题,具有良好的稳定性。
但在最初提出的五点要求中,该函数仅满足了前四点。问题出在第五点,由于JavaScript规范未规定for/in语句的执行顺序必须与赋值顺序一致,尽管多数情况下符合预期,但在不同的JavaScript版本或引擎实现中,执行顺序可能存在差异,导致Event对象标准化逻辑的不确定性。
改进版通用绑定函数
最后,展示对Dean Edward addEvent()函数的改进版本。
Array.prototype.indexOf = function( obj ){
var result = -1 , length = this.length , i=length - 1;
for ( ; i>=0 ; i-- ) {
if ( this[i] == obj ) {
result = i;
break;
}
}
return result;
}
Array.prototype.contains = function( obj ) {
return ( this.indexOf( obj ) >=0 )
}
Array.prototype.append = function( obj , nodup ) {
if ( !(nodup && this.contains( obj )) ) {
this[this.length] = obj;
}
}
Array.prototype.remove = function( obj ) {
var index = this.indexOf( obj );
if ( !index ) return ;
return this.splice( index , 1);
};
function addEvent(element , type , fun){
if (!element.events) element.events = {};
var handlers = element.events[type];
if (!handlers) {
handlers = element.events[type] = [];
if(element['on' + type]) {
handlers[0] = element['on' + type];
}
}
handlers.append( fun , true)
element['on' + type] = handleEvent;
}
function removeEvent(element , type , fun) {
if (element.events && element.events[type]) {
element.events[type].remove(fun);
}
}
function handleEvent(event) {
var returnValue = true , i=0;
event = event || fixEvent(window.event);
var handlers = this.events[event.type] , length = handlers.length;
for ( ; i < length ; i++) {
if ( handlers[i].call( this , event) === false ){
returnValue = false;
}
}
return returnValue;
}
function fixEvent(event) {
event.preventDefault = fixEvent.preventDefault;
event.stopPropagation = fixEvent.stopPropagation;
return event;
}
fixEvent.preventDefault = function() {
this.returnValue = false;
};
fixEvent.stopPropagation = function() {
this.cancelBubble = true;
};
该改进版函数针对前述问题进行了优化,完全满足了最初提出的五点核心要求。希望这一方案能为读者在复杂浏览器环境下的事件处理提供有效帮助,感谢阅读。