JavaScript事件处理程序及事件对象的跨浏览器实现

2011-09-19

事件处理程序的浏览器兼容性分析

事件处理程序有好几种,不过常用的就是HTML事件处理程序、DOM0级事件处理程序和DOM2级事件处理程序。对于前两种基本上没有浏览器兼容性问题。但是对于DOM2级事件处理程序,由于IE没有按照DOM2标准走,而是自己实现了类似的方法,所以就有问题了。下面分析一下。

DOM2级事件定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener()和removeEventListener()。所有DOM节点都包含这两个方法,并且它们都接受3个参数:需要处理的事件名、作为事件处理程序的函数和一个布尔值。最后的布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。

IE则实现了两个类似的方法:attachEvent()和detachEvent()。这两个方法接受相同的两个参数:事件处理程序名称事件处理程序函数。由于IE只支持事件冒泡,所以这两个方法均没有第三个参数,而且所有事件处理程序都会被添加到冒泡阶段。

需要处理的事件名参数

在DOM2级事件中需要处理的事件名是真实的事件名,如:”click”、”load”等,而在IE中则需要加上”on”,例如:”onclick”、”onload”等。

鼠标滚轮的滚动事件由于没有DOM标准,所以也有兼容性问题。在FireFox中是DOMMouseScroll,而在其他浏览器中则是mousewheel。还有其事件对象也不一样,这个下面再说。

触发顺序

两种实现都支持绑定多个事件处理程序绑定到同一个事件上,但是事件处理程序的触发顺序却是相反的。DOM2级事件是按照先绑定先触发执行的,而IE中则是后绑定先触发的顺序。

this引用

在DOM2级事件的事件处理函数中的this指向的是当前元素,而在IE中this指向的是window对象。

<script type="text/javascript">
window.onload = function() {
    var btn = document.getElementById("myBtn");
    if (btn.addEventListener) {
        btn.addEventListener("click", function() {
            alert(this.id);
        }, false);
    } else {
        btn.attachEvent("onclick", function() {
            alert(this === window);
        });
    }
}
</script>
<input type="button" id="myBtn" value="click" />

上面的代码如果在非IE中执行的话,就会显示”myBtn”,而在IE中执行的话就会显示”true”。

事件对象

兼容DOM的浏览器会将一个event对象传入到事件处理程序中,无论指定事件处理程序时使用什么方法,都会传入event对象。而而在IE中却更复杂很多,不同的事件处理程序指定方法,其处理不一样。使用DOM0级方法添加事件处理程序时,event对象作为window对象的一个属性存在;使用attachEvent()添加事件处理程序时,会有一个event对象作为参数被传入事件处理函数中,当然也可以通过window.event来访问;使用HTML特性指定的事件处理程序则可以通过event的变量来访问事件对象。例如:

<script type="text/javascript">
window.onload = function() {
    var btn = document.getElementById("myBtn");
    if (btn.addEventListener) {
        btn.addEventListener("click", function(event) {
            alert(event.type);
        }, false);
    } else {
        btn.attachEvent("onmouseout", function(event) {
            alert(event.type + " " + window.event.type);
        });
        btn.onmouseover = function() {
            alert(window.event.type);
        };
    }
}
</script>
<input type="button" id="myBtn" value="click" onclick="alert(event.type)"/>

事件对象的浏览器兼容性分析

DOM中的事件对象

事件对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。不过,所有事件都会有下表列出的成员。

属性/方法类型读/写说明
bubblesBoolean只读表明事件是否冒泡
cancelableBoolean只读表明是否可以取消事件的默认行为
currentTargetElement只读其事件处理程序当前正在处理事件的那个元素
detailInteger只读与事件相关的细节信息
eventPhaseInteger只读调用事件处理程序的阶段:1为捕获阶段,2为处于目标,3为冒泡阶段
preventDefault()Function只读取消事件的默认行为,如果cancelable是true,则可以使用这个方法。
stopPropagation()Function只读取消事件的进一步捕获或冒泡,如果bubbles为true,则可以使用这个方法
targetElement只读事件的目标
typeString只读被触发事件的类型
viewAbstractView只读与事件关联的抽象视图,等同于发生事件的window对象

IE中的事件对象

IE中的事件对象同样也包括与创建它的事件相关的属性和方法。其中很多属性和方法都有对应的或者相关的DOM属性和方法。与DOM的event对象一样,这些属性和方法也会因为事件类型的不同而不同,但所有事件对象都会包含下表列出的属性和方法。

属性/方法类型读/写说明
cancelBubbleBoolean读/写默认值为false,但将其设置为true就可以取消事件冒泡(与DOM中的stopPropagation()方法的作用相同)
returnValueBoolean读/写默认值为true,但将其设置为false就可以取消默认行为(与DOM中的preventDefault()方法的作用相同)
srcElementElement只读事件的目标(与DOM中的target属性相同)
typeString只读被触发事件的类型(不包含”on”)

鼠标滚轮滚动事件的事件对象

在鼠标滚轮的事件对象中包含一个Integer类型的属性,其值标识了鼠标的滚动方向。在FireFox中是保存detail属性中,当滚轮向前滚动时,这个值是-3的倍数,向后滚动时,这个值是3的倍数;而在其他浏览器中则是wheelDelta属性,当滚轮向前滚动时,这个值是120的倍数,想后滚动时,这个值是-120的倍数。

keypress事件的事件对象

在FireFox,Chrome和Safari的事件对象都支持一个charCode属性,这个属性的值是按下的那个建所代表字符的ASCII编码,而在IE和Opera中则是在keyCode中保存字符的ASCII编码。

事件处理程序及事件对象的跨浏览器实现

根据以上对浏览器兼容性分析,编码实现了基本按照DOM标准的事件处理程序及事件对象,消除了添加事件、移除事件的差异,事件名有没有on的差异,将IE中的this引用指向了当前元素,事件对象全部修正为通过参数传递,将IE中的事件对象尽量修正成DOM标准形式,修正了FireFox中的鼠标滚轮滚动的事件对象。但是对于IE中的多事件处理程序绑定的触发顺序没有修正。代码如下:

var EventUtil = (function() {
    var EventUtil, //事件处理对象
        eventList = [], //事件列表
        //浏览器兼容的鼠标滚动事件type
        mouseWheelType = typeof window.opera !== "object" && !/(KHTML|AppleWebKit|Konqueror)/.test(navigator.userAgent) && /Gecko/.test(navigator.userAgent) ? "DOMMouseScroll" : "mousewheel",
        //鼠标滚动事件type正则
        mouseWheelRegExp = /^mousewheel|DOMMouseScroll$/;
    //事件详细信息构造器

    function EventDetail(element, type, handler, proxy) {
        this.element = element;
        this.type = type;
        this.handler = handler;
        this.proxy = proxy;
    }
    //将事件对象处理成DOM标准的事件对象
    function eventObjHandler(event, element) {
        event = event || window.event;
        if (!event.target) {
            event.target = event.srcElement;
        }
        if (event.cancelable === undefined) {
            event.cancelable = true;
        }
        if (!event.preventDefault) {
            event.preventDefault = function() {
                event.returnValue = false;
            };
        }
        if (event.bubbles === undefined) {
            event.bubbles = true;
        }
        if (!event.stopPropagation) {
            event.stopPropagation = function() {
                event.cancelBubble = true;
            };
        }
        if (!event.currentTarget) {
            event.currentTarget = element;
        }
        if (!event.eventPhase) {
            event.eventPhase = event.target === event.currentTarget ? 2 : 3;
        }
        if (mouseWheelRegExp.test(event.type) && !event.wheelDelta) {
            event.wheelDelta = -event.detail * 40;
        }
        return event;
    }
    //创建handler的代理函数,用于修正this引用
    function createProxyFunc(ele, type, handler) {
        var proxy = function(event) {
            handler.call(ele, eventObjHandler(event, ele));
        };
        eventList.push(new EventDetail(ele, type, handler, proxy));
        return proxy;
    }
    //从eventList中找出真正绑定到事件中的处理函数,并删除相应的EventDetail
    function findTheTrueEventHandler(ele, type, handler) {
        var i, eventDetail;
        for (i = eventList.length - 1; i >= 0; i--) {
            eventDetail = eventList[i];
            if (eventDetail.element === ele && eventDetail.type === type && eventDetail.handler === handler) {
                break;
            }
        }
        if (i > -1) {
            handler = eventList[i].proxy;
            eventList.splice(i, 1);
        }
        return handler;
    }
    //修正鼠标滚轮事件名
    function repairMouseWheel(type) {
        return mouseWheelRegExp.test(type) ? mouseWheelType : type;
    }
    EventUtil = {
        //添加事件绑定
        add: function(ele, type, handler) {
            type = repairMouseWheel(type); //修正type
            if (ele.addEventListener) { //标准
                ele.addEventListener(type,
                    type === "DOMMouseScroll" ? createProxyFunc(ele, type, handler) : handler, false);
            } else if (ele.attachEvent) { //IE
                ele.attachEvent("on" + type, createProxyFunc(ele, type, handler));
            } else { //DOM1
                ele["on" + type] = function(event) {
                    handler.call(ele, eventObjHandler(event));
                };
            }
        },
        //移除事件绑定
        remove: function(ele, type, handler) {
            type = repairMouseWheel(type); //修正type
            if (ele.removeEventListener) { //标准
                ele.removeEventListener(type,
                    type === "DOMMouseScroll" ? findTheTrueEventHandler(ele, type, handler) : handler, false);
            } else if (ele.detachEvent) { //IE
                ele.detachEvent("on" + type, findTheTrueEventHandler(ele, type, handler));
            } else { //DOM1
                ele["on" + type] = null;
            }
        }
    };
    //绑定页面关闭事件,当页面关闭时移除所有事件
    EventUtil.add(window, "unload", function() {
        var i, eventDetail;
        for (i = eventList.length - 1; i >= 0; i--) {
            eventDetail = eventList[i];
            if (eventDetail.type === "DOMMouseScroll") {
                eventDetail.element.removeEventListener(eventDetail.type, eventDetail.proxy, false);
            } else {
                eventDetail.element.detachEvent("on" + eventDetail.type, eventDetail.proxy, false);
            }
            eventList[i] = null;
        }
        eventList = [];
    });
    return EventUtil;
})();

这里利用了函数闭包的方式定义了一个EventUtil对象,里面包含了两个方法(add,remove)。使用很简单,只需要调用EventUtil.add(element,type,handler)方法即可指定事件处理程序,通过remove移除事件处理程序。上面通过使用call调用方法修正了this引用,通过eventObjHanler()方法将event对象处理成了类DOM标准的事件对象。最后通过绑定unload事件,在页面卸载时会将所有保存在eventList中的事件处理程序全部移除掉。示例:

window.onload = function() {
    EventUtil.add(document.getElementById("myBtn"), "click", clickHandler);
    EventUtil.add(document.getElementById("removeBtn"), "click", clickHandler);
    EventUtil.add(document, "mousewheel", mousewheelHandler);
}

function clickHandler(e) {
    if (this.id == "myBtn") {
        alert(this.id + " has clicked!"); //myBtn has clicked!
    } else {
        EventUtil.remove(document.getElementById("myBtn"), "click", clickHandler);
        alert("myBtn's Event has removed!");
    }
}

function mousewheelHandler(e) {
    alert(e.wheelDelta); //120 | -120
}

以上代码能兼容IE、FireFox、Google Chrome等浏览器,并表现一致。通过点击id为removeBtn的按钮将会移除前面为id为myBtn的按钮上的click事件。

参考资料:《JavaScript高级程序设计(第2版)》

Update:2011-11-18,将原来基于对象字面量的实现修改为通过闭包实现。