JavaScript 跨浏览器事件系统实现详解

作者:窗边听雨时间:2026-10-24 18:19:20浏览:490

本文深入解析 JavaScript 跨浏览器事件系统的实现原理,针对 IE 与标准浏览器在事件模型、对象属性及执行顺序上的差异,提供了一套完整的兼容方案。通过封装 attachEvent 与 handleEvent 等方法,统一处理事件对象修正、冒泡控制及回调管理,解决循环引用与绑定冲突问题,适用于需要高度兼容性的前端开发场景。

跨浏览器事件系统背景与挑战

尽管 JavaScript 之父无法垄断所有浏览器市场,网景也未强大到迫使竞争对手采用其标准,但市场上仍存在多种实现,如微软的 JScript、已故 Macromedia 的 ActionScript 等,分支繁杂。利用浏览器内置的 DOM 事件模型会带来三个主要问题:首先,必须依赖某个 DOM 对象(如 window、document 或元素节点)才能使用;其次,各浏览器对 DOM 的支持程度不一,导致事件模型缺乏一致性;最后,基于 DOM 对象容易引发循环引用问题。在微软赢得第一次浏览器大战后,其 DOM 模型基本停止更新,而“标准浏览器”则持续向 W3C 和 ECMA 标准靠拢,形成两大阵营。即便在标准浏览器内部也存在差异,例如 Firefox 不支持 mousewheel 而使用 DOMMouseScroll,Opera 的 contextmenu 则不可控。因此,我们需要自行实现一套事件系统。目前,主流浏览器均实现了 DOM2 级事件模型,微软采用 attachEvent,标准浏览器采用 addEventListener,允许同一元素绑定多个同类型事件回调。网上常见的 addEvent 函数多基于此实现,但并不可靠:首先,IE 的回调函数未强制绑定事件对象,而标准浏览器强制将第一个参数作为事件对象,尽管可通过 call 函数实现强制绑定,但 IE 的事件对象结构与标准浏览器不同,处理工作复杂;其次,回调函数的执行顺序在 IE 中无规律,而在标准浏览器中按绑定先后顺序执行。因此,这两个函数均被排除。我打算使用最原始的 onXXXX 方式实现,在绑定多个函数时,将它们放入一个函数中,通过一个 for 循环完成处理。


代码如下:

 
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta content="IE=8" http-equiv="X-UA-Compatible"/>
<title>Event系统 by 司徒正美</title>
<style type="text/css">
#target{
width:400px;
height:100px;
background:blue;
} 
</style>
<script type="javascript">
var dom = {};
Array.prototype.indexOf = function (el, index) {
var n = this.length>>>0,
i = index == null ? 0 : index < 0 ? Math.max(0, n + index) : index;
for (; i < n; i++)
if (i in this && this[i] === el) return i;
return -1;
}; 
//http://msdn.microsoft.com/zh-cn/library/bb383786.aspx
//移除 Array 对象中某个元素的第一个匹配项。
Array.prototype.remove= function (item) {
var index = this.indexOf(item);
if (index !== -1) return this.removeAt(index);
return null;
}; 
//移除 Array 对象中指定位置的元素。
Array.prototype.removeAt= function (index) {
return this.splice(index, 1)
}; 
dom.attachEvent = function(el, type, handler) {
// 在每个元素上设置一个Object类型的私定义属性events
if (!el.events) el.events = {};
// 这个对象有许多键为事件类型,值为函数数组的属性
var handlers = el.events[type];
if (!handlers) {
handlers = el.events[type] = [];
// 如果它原来就以onXXXX方式绑定了某事件,那么把它置为事件数组的第一个元素
if (el["on" + type]) {
handlers[0] = el["on" + type];
} 
} 
//添加回调函数
handlers.push(handler)
//以onXXXX方式绑定我们的处理函数
el["on" + type] = dom.handleEvent;
}; 
dom.detachEvent = function(el, type, handler) {
if (el.events && el.events[type]) {
el.events[type].remove(handler)
} 
} 
dom.handleEvent = function (event) {
var returnValue = true;
// grab the event object (IE uses a global event object)
event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
// get a reference to the hash table of event handlers
var handlers = this.events[event.type];
// execute each event handler
for(var i=0,n=handlers.length;i<n;i++){
if (handlers[i](event) === false) {
returnValue = false;
} 
} 
return returnValue;
}; 
function fixEvent(event) {
// add W3C standard event methods
event.preventDefault = fixEvent.preventDefault;
event.stopPropagation = fixEvent.stopPropagation;
return event;
}; 
fixEvent.preventDefault = function() {
this.returnValue = false;
}; 
fixEvent.stopPropagation = function() {
this.cancelBubble = true;
}; 
var $ = function(id){
return document.getElementById(id)
} 
window.onload = function(){
var a = function(e){
$("p").innerHTML = e.clientX +" "+e.clientY
} 
dom.attachEvent($("target"),"mousemove",a);
setTimeout(function(){
dom.detachEvent($("target"),"mousemove",a);
},10*1000)
} 
</script>
</head>
<body>
<div id="target">
</div>
<p id="p"></p>
</body>
</html>

基础事件绑定与卸载实现

回顾上述流程,该事件系统实为 Dean 大神 addEvent 的改版。


首先设置一个作为命名空间的对象。


对 Array 进行扩展。


attachEvent 函数用于绑定事件。具体做法在需要绑定事件的对象设置一个events属性,里面再按事件类型放置回调函数,由于有时我们可能在同一个元素上绑定2个或多个onclick事件什么的,因此它们必须是一个数组。最后用DOM0的原始方法添加一个onXXXX属性。


detachEvent 函数用于卸载事件,即从 events 对应类型的数组中移除元素。


handleEvent 执行回调函数。我们通过 onXXXX 形式绑定了一个全局函数,其作用是获取并修正事件对象,然后取得该事件类型对应的所有回调函数,依次将事件对象作为第一个参数执行它们,最后处理冒泡。


fixEvent 修正事件对象,主要目的是让 IE 拥有标准浏览器的两个方法。


高级事件对象修正与伪事件处理

对于一般应用,上述方案已足够。但若追求完美,仍有改进空间。首先,将庞大的 events 对象直接放置在元素上并不妥当,不利于集中管理。其次,fixEvent 并不彻底,例如 target、pageX/Y 等标准浏览器下的属性,在 IE 中仍无法使用。


首先是 handleEvent 函数,现在无论标准浏览器还是 IE 的事件对象都需要修正,且在每次调用时为 IE 修正其 currentTarget 值。


代码如下:

 
dom.handleEvent = function (event) {
event = event || window.event
event = dom.fixEvent(event);
event.currentTarget = this;//修正currentTarget
var returnValue = true;
var handlers = this.events[event.type];
for(var i=0,n=handlers.length;i<n;i++){
if (handlers[i](event) === false) {
returnValue = false;
} 
} 
return returnValue;
}; 

在介绍新版 fixEvent 函数前,先隆重介绍我从 jQuery 借鉴的伪事件对象。


代码如下:

 
dom.oneObject = function(arr,val){
var result = {},value = val !== undefined ? val :1;
for(var i=0,n=arr.length;i<n;i++)
result[arr[i]] = value;
return result;
}; 
dom.mixin = function(result, source) {
if (arguments.length === 1) {
source = result;
result = dom;
} 
if (result && source ){
for(var key in source)
source.hasOwnProperty(key) && (result[key] = source[key]);
} 
if(arguments.length > 2 ){
var others = [].slice.call(arguments,2);
for(var i=0,n=others.length;i<n;i++){
result = arguments.callee(result,others[i]);
} 
} 
return result;
} 
var MouseEventOne = dom.oneObject(["click","dblclick","mousedown",
"mousemove","mouseout", "mouseover","mouseup"],"[object MouseEvent]");
var HTMLEventOne = dom.oneObject(["abort","blur","change","error","focus",
"load","reset","resize","scroll","select","submit","unload"],"[object Event]");
var KeyboardEventOne = dom.oneObject(["keyup","keydown","keypress",],
"[object KeyboardEvent]");
var EventMap = dom.mixin({},MouseEventOne,HTMLEventOne,KeyboardEventOne)
var fn = "prototype";
dom.Event = function( src ) {
if ( !this.preventDefault ) {
return new dom.Event[fn].init( src );
} 
}; 
function returnFalse() {
return false;
} 
function returnTrue() {
return true;
} 
// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
dom.Event[fn] = {
init:function(src){
//如果传入的是事件对象
if ( src && src.type ) {
this.originalEvent = src;
this.type = src.type;
//如果传入的是事件类型
} else {
this.type = src;
} 
this.timeStamp = new Date().valueOf();
this[ "expando" ] = true;
}, 
//http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/events.html#Conformance
toString:function(){
return EventMap[this.type] || "[object Event]"
}, 
preventDefault: function() {
this.isDefaultPrevented = returnTrue;
var e = this.originalEvent;
if ( !e ) {
return;
} 
// 如果存在preventDefault 那么就调用它
if ( e.preventDefault ) {
e.preventDefault();
} 
// 如果存在returnValue 那么就将它设为false
e.returnValue = false;
}, 
stopPropagation: function() {
this.isPropagationStopped = returnTrue;
var e = this.originalEvent;
if ( !e ) {
return;
} 
// 如果存在preventDefault 那么就调用它
if ( e.stopPropagation ) {
e.stopPropagation();
} 
// 如果存在returnValue 那么就将它设为true
e.cancelBubble = true;
}, 
stopImmediatePropagation: function() {
this.isImmediatePropagationStopped = returnTrue;
this.stopPropagation();
}, 
isDefaultPrevented: returnFalse,
isPropagationStopped: returnFalse,
isImmediatePropagationStopped: returnFalse
}; 
dom.Event[fn].init[fn] = dom.Event[fn];

该构造函数仅实现了 W3C 事件模型的少数方法,其余属性将在 fixEvent 方法中通过拷贝方式实现。为区分原生事件对象与伪事件对象,我们在其上添加了一个 expando 属性。


代码如下:

 
var buttonMap = {
1:1,
4:2,
2:3
} 
dom.fixEvent = function(event){
if ( event[ "expando" ] ) {
return event;
} 
var originalEvent = event
event = dom.Event(originalEvent);
for(var prop in originalEvent){
if(typeof originalEvent[prop] !== "function"){
event[prop] = originalEvent[prop]
} 
} 
//如果不存在target属性,为它添加一个
if ( !event.target ) {
event.target = event.srcElement || document;
} 
//如果事件源对象为文本节点,则置入其父元素
if ( event.target.nodeType === 3 ) {
event.target = event.target.parentNode;
} 
//如果不存在relatedTarget属性,为它添加一个
if ( !event.relatedTarget && event.fromElement ) {
event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
} 
//如果不存在pageX/Y则结合clientX/Y做一双出来
if ( event.pageX == null && event.clientX != null ) {
var doc = document.documentElement, body = document.body;
event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
} 
// 为键盘事件添加which事件
if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) ) {
event.which = event.charCode || event.keyCode;
} 
// Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
if ( !event.metaKey && event.ctrlKey ) {
event.metaKey = event.ctrlKey;
} 
// 判定鼠标事件按下的是哪个键,1 === left; 2 === middle; 3 === right
if ( !event.which && event.button !== undefined ) {
event.which = buttonMap[event.button]
} 
return event;
} 

毫不犹豫地采用 jQuery 的方法,因为在所有类库中,jQuery 的方法最易于提取。


现在我们基本解决了文章中段提出的两个问题之一。要解决第一个问题,我们需要引入缓存系统,这将在下一部分讲解。


 <!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta content="IE=8" http-equiv="X-UA-Compatible"/> <title>Event by 司徒正美</title> <style type="text/css"> #target{ width:400px; height:100px; background:blue; } </style>  </head> <body> <div id="target" > </div> <p id="p">显示鼠标位置</p> <h1>请按下键盘的键</h1> </body> </html> 

[Ctrl+A 全选 注:引入外部Js需再刷新一下页面才能执行]

作者最新文章