javascript打造跨浏览器事件处理机制[Blue-Dream出品]

使用类库可以比较容易的解决兼容性问题.但这背后的机理又是如何呢? 下面我们就一点点铺开来讲.

首先,DOM Level2为事件处理定义了两个函数addEventListenerremoveEventListener, 这两个函数都来自于EventTarget接口. 


代码如下:

element.addEventListener(eventName, listener, useCapture);
element.removeEventListener(eventName, listener, useCapture);

EventTarget接口通常实现自Node或Window接口.也就是所谓的DOM元素.
那么比如window也就可以通过addEventListener来添加监听.


代码如下:

function loadHandler() {
console.log('the page is loaded!');
}
window.addEventListener('load', loadHandler, false);

移除监听通过removeEventListener同样很容易做到, 只要注意移除的句柄和添加的句柄引用自一个函数就可以了.
window.removeEventListener('load', loadHandler, false);

如果我们活在完美世界.那么估计事件函数就此结束了.
但情况并非如此.由于IE独树一帜.通过MSDHTML DOM定义了attachEvent和detachEvent两个函数取代了addEventListener和removeEventListener.
恰恰函数间又存在着很多的差异性,使整个事件机制变得异常复杂.
所以我们要做的事情其实就转移成了.处理IE浏览器和w3c标准之间对于事件处理的差异性.

在IE下添加监听和移除监听可以这样写


代码如下:

function loadHandler() {
alert('the page is loaded!');
}
window.attachEvent('onload', loadHandler); // 添加监听
window.detachEvent('onload', loadHandler); // 移除监听

从表象看来,我们可以看出IE与w3c的两处差异:
1. 事件前面多了个"on"前缀.
2. 去除了useCapture第三个参数.
其实真正的差异远远不止这些.等我们后面会继续分析.那么对于现在这两处差异我们很容易就可以抽象出一个公用的函数


代码如下:

function addListener(element, eventName, handler) {
if (element.addEventListener) {
element.addEventListener(eventName, handler, false);
}
else if (element.attachEvent) {
element.attachEvent('on' + eventName, handler);
}
else {
element['on' + eventName] = handler;
}
}
function removeListener(element, eventName, handler) {
if (element.addEventListener) {
element.removeEventListener(eventName, handler, false);
}
else if (element.detachEvent) {
element.detachEvent('on' + eventName, handler);
}
else {
element['on' + eventName] = null;
}
}

上面函数有两处需要注意一下就是:
1. 第一个分支最好先测定w3c标准. 因为IE也渐渐向标准靠近. 第二个分支监测IE.
2. 第三个分支是留给既不支持(add/remove)EventListener也不支持(attach/detach)Event的浏览器.

性能优化
对于上面的函数我们是运用"运行时"监测的.也就是每次绑定事件都需要进行分支监测.我们可以将其改为"运行前"就确定兼容函数.而不需要每次监测.
这样我们就需要用一个DOM元素提前进行探测. 这里我们选用了document.documentElement. 为什么不用document.body呢? 因为document.documentElement在document没有ready的时候就已经存在. 而document.body没ready前是不存在的.
这样函数就优化成


代码如下:

var addListener, removeListener,
/* test element */
docEl = document.documentElement;
// addListener
if (docEl.addEventListener) {
/* if `addEventListener` exists on test element, define function to use `addEventListener` */
addListener = function (element, eventName, handler) {
element.addEventListener(eventName, handler, false);
};
}
else if (docEl.attachEvent) {
/* if `attachEvent` exists on test element, define function to use `attachEvent` */
addListener = function (element, eventName, handler) {
element.attachEvent('on' + eventName, handler);
};
}
else {
/* if neither methods exists on test element, define function to fallback strategy */
addListener = function (element, eventName, handler) {
element['on' + eventName] = handler;
};
}
// removeListener
if (docEl.removeEventListener) {
removeListener = function (element, eventName, handler) {
element.removeEventListener(eventName, handler, false);
};
}
else if (docEl.detachEvent) {
removeListener = function (element, eventName, handler) {
element.detachEvent('on' + eventName, handler);
};
}
else {
removeListener = function (element, eventName, handler) {
element['on' + eventName] = null;
};
}

这样就避免了每次绑定都需要判断.
值得一提的是.上面的代码其实也是有两处硬伤. 除了代码量增多外, 还有一点就是使用了硬性编码推测.上面代码我们基本的意思就是断定.如果document.documentElement具备了add/remove方法.那么element就一定具备(虽然大多数情况如此).但这显然是不够安全.
不安全的检测
下面两个例子说明.在某些情况下这种检测不是足够安全的.


代码如下:

// In Internet Explorer
var xhr = new ActiveXObject('Microsoft.XMLHTTP');
if (xhr.open) { } // Error
var element = document.createElement('p');
if (element.offsetParent) { } // Error

如: 在IE7下 typeof xhr.open === 'unknown'. 详细可参考feature-detection
所以我们提倡的检测方式是


代码如下:

var isHostMethod = function (object, methodName) {
var t = typeof object[methodName];
return ((t === 'function' || t === 'object') && !!object[methodName]) || t === 'unknown';
};

这样我们上面的优化函数.再次改进成这样


代码如下:

var addListener, docEl = document.documentElement;
if (isHostMethod(docEl, 'addEventListener')) {
/* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
/* ... */
}
else {
/* ... */
}

丢失的this指针
this指针的处理.IE与w3c又出现了差异.在w3c下函数的指针是指向绑定该句柄的DOM元素. 而IE下却总是指向window.


代码如下:

// IE
document.body.attachEvent('onclick', function () {
alert(this === window); // true
alert(this === document.body); // false
});
// W3C
document.body.addEventListener('onclick', function () {
alert(this === window); // false
alert(this === document.body); // true
});

这个问题修正起来也不算麻烦


代码如下:

if (isHostMethod(docEl, 'addEventListener')) {
/* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
addListener = function (element, eventName, handler) {
element.attachEvent('on' + eventName, function () {
handler.call(element, window.event);
});
};
}
else {
/* ... */
}

我们只需要用一个包装函数.然后在内部将handler用call重新修正指针.其实大伙应该也看出了,这里还偷偷的修正了一个问题就是.IE下event不是通过第一个函数传递,而是遗留在全局.所以我们经常会写event = event || window.event这样的代码. 这里也一并做了修正.
修正了这几个主要的问题.我们这个函数看起来似乎健壮了很多.我们可以暂停一下做下简单的测试, 测试三点
1. 各浏览器兼容 2. this指针指向兼容 3. event参数传递兼容.

测试代码如下:

Event Test UseCase

测试文本

var isHostMethod = function (object, methodName) {
var t = typeof object[methodName];
return ((t === 'function' || t === 'object') && !!object[methodName]) || t === 'unknown';
};
var addListener, removeListener,
/* test element */
docEl = document.documentElement;
if (isHostMethod(docEl, 'addEventListener')) {
addListener = function (element, eventName, handler) {
element.addEventListener(eventName, handler, false);
};
}
else if (isHostMethod(docEl, 'attachEvent')) {
addListener = function (element, eventName, handler) {
element.attachEvent('on' + eventName, function () {
handler.call(element, window.event);
});
};
}
else {
addListener = function (element, eventName, handler) {
element['on' + eventName] = handler;
};
}
if (isHostMethod(docEl, 'removeEventListener')) {
removeListener = function (element, eventName, handler) {
element.removeEventListener(eventName, handler, false);
};
}
else if (isHostMethod(docEl, 'detachEvent')) {
removeListener = function (element, eventName, handler) {
element.detachEvent('on' + eventName, handler);
};
}
else {
removeListener = function (element, eventName, handler) {
element['on' + eventName] = null;
};
}

// Test UseCase
var o = document.getElementById('odiv');
addListener(o, 'click', function(event) {
this.style.backgroundColor = 'blue';
alert((event.target || event.srcElement).innerHTML);
});

[Ctrl+A 全选 注:如需引入外部Js需刷新才能执行]

我们只需这样调用方法:


代码如下:

addListener(o, 'click', function(event) {
this.style.backgroundColor = 'blue';
alert((event.target || event.srcElement).innerHTML);
});

可见'click' , this, event 都做到了浏览器一致性. 这样是不是我们就万事大吉了?
其实这只是万里长征的第一步.由于IE浏览器下和谐的内存泄露,使我们的事件机制要考虑的比上面复杂的多.
看下我们上面的一处修正this指针的代码
element.attachEvent('on' + eventName, function () {
handler.call(element, window.event);
});
element --> handler --> element 很容易的形成了个循环引用. 在IE下就内存泄露了.
解除循环引用
解决内存泄露的方法就是切断循环引用. 也就是将handler --> element这段引用给切断. 很容易想到的方法,也是至今还有很多类库在使用的方法.就是在window窗体unload的时候将所有handler指向null .
基本代码如下
代码


代码如下:

function wrapHandler(element, handler) {
return function (e) {
return handler.call(element, e || window.event);
};
}
function createListener(element, eventName, handler) {
return {
element: element,
eventName: eventName,
handler: wrapHandler(element, handler)
};
}
function cleanupListeners() {
for (var i = listenersToCleanup.length; i--; ) {
var listener = listenersToCleanup[i];
litener.element.detachEvent(listener.eventName, listener.handler);
listenersToCleanup[i] = null;
}
window.detachEvent('onunload', cleanupListeners);
}
var listenersToCleanup = [ ];
if (isHostMethod(docEl, 'addEventListener')) {
/* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
addListener = function (element, eventName, handler) {
var listener = createListener(element, eventName, handler);
element.attachEvent('on' + eventName, listener.handler);
listenersToCleanup.push(listener);
};
window.attachEvent('onunload', cleanupListeners);
}
else {
/* ... */
}

也就是将listener用数组保存起来.在window.unload的时候循环一次全部指向为null.从此切断引用.
这看起来是个很不错的方法.很好的解决了内存泄露问题.
避免内存泄露
在我们刚刚要松口气的时候.又一个令人咂舌的事情发生了.bfcache这个被大多主流浏览器实现的页面缓存机制.介绍上赫然写了几条会导致缓存失效的几个条款
the page uses an unload or beforeunload handler
the page sets "cache-control: no-store"
the page sets "cache-control: no-cache" and the site is HTTPS.
the page is not completely loaded when the user navigates away from it
the top-level page contains frames that are not cacheable
the page is in a frame and the user loads a new page within that frame (in this case, when the user navigates away from the page, the content that was last loaded into the frames is what is cached)
第一条就是说我们伟大的unload会杀掉页面缓存.页面缓存的作用就是.我们每次点前进后退按钮都会从缓存读取而不需每次都去请求服务器.这样一来就矛盾了...
我们既想要页面缓存.但又得切断内存泄露的循环引用.但却又不能使用unload事件...
最后只能使用终极方案.就是禁止循环引用
这个方案仔细介绍起来也很麻烦.但如果见过DE大神最早的事件函数.应该理解起来就不难了. 总结起来需要做以下工作.
1. 为每个element指定一个唯一的uniqueID.
2. 用一个独立的函数来创建监听. 但这个函数不直接引用element, 避免循环引用.
3. 创建的监听与独立的uid和eventName相结合
4. 通过attachEvent去触发包装的事件句柄.
经过上面的一系列分析.我们得到了最终的这个相对最完美的事件函数


代码如下:

(function(global) {
// 判断是否具有宿主属性
function areHostMethods(object) {
var methodNames = Array.prototype.slice.call(arguments, 1),
t, i, len = methodNames.length;
for (i = 0; i < len; i++) {
t = typeof object[methodNames[i]];
if (!(/^(?:function|object|unknown)$/).test(t)) return false;
}
return true;
}
// 获取唯一ID
var getUniqueId = (function() {
if (typeof document.documentElement.uniqueID !== 'undefined') {
return function(element) {
return element.uniqueID;
};
}
var uid = 0;
return function(element) {
return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++);
};
})();
// 获取/设置元素标志
var getElement, setElement;
(function() {
var elements = {};
getElement = function(uid) {
return elements[uid];
};
setElement = function(uid, element) {
elements[uid] = element;
};
})();
// 独立创建监听
function createListener(uid, handler) {
return {
handler: handler,
wrappedHandler: createWrappedHandler(uid, handler)
};
}
// 事件句柄包装函数
function createWrappedHandler(uid, handler) {
return function(e) {
handler.call(getElement(uid), e || window.event);
};
}
// 分发事件
function createDispatcher(uid, eventName) {
return function(e) {
if (handlers[uid] && handlers[uid][eventName]) {
var handlersForEvent = handlers[uid][eventName];
for (var i = 0, len = handlersForEvent.length; i < len; i++) {
handlersForEvent[i].call(this, e || window.event);
}
}
}
}
// 主函数体
var addListener, removeListener,
shouldUseAddListenerRemoveListener = (
areHostMethods(document.documentElement, 'addEventListener', 'removeEventListener') &&
areHostMethods(window, 'addEventListener', 'removeEventListener')),
shouldUseAttachEventDetachEvent = (
areHostMethods(document.documentElement, 'attachEvent', 'detachEvent') &&
areHostMethods(window, 'attachEvent', 'detachEvent')),
// IE branch
listeners = {},
// DOM L0 branch
handlers = {};
if (shouldUseAddListenerRemoveListener) {
addListener = function(element, eventName, handler) {
element.addEventListener(eventName, handler, false);
};
removeListener = function(element, eventName, handler) {
element.removeEventListener(eventName, handler, false);
};
}
else if (shouldUseAttachEventDetachEvent) {
addListener = function(element, eventName, handler) {
var uid = getUniqueId(element);
setElement(uid, element);
if (!listeners[uid]) {
listeners[uid] = {};
}
if (!listeners[uid][eventName]) {
listeners[uid][eventName] = [];
}
var listener = createListener(uid, handler);
listeners[uid][eventName].push(listener);
element.attachEvent('on' + eventName, listener.wrappedHandler);
};
removeListener = function(element, eventName, handler) {
var uid = getUniqueId(element), listener;
if (listeners[uid] && listeners[uid][eventName]) {
for (var i = 0, len = listeners[uid][eventName].length; i < len; i++) {
listener = listeners[uid][eventName][i];
if (listener && listener.handler === handler) {
element.detachEvent('on' + eventName, listener.wrappedHandler);
listeners[uid][eventName][i] = null;
}
}
}
};
}
else {
addListener = function(element, eventName, handler) {
var uid = getUniqueId(element);
if (!handlers[uid]) {
handlers[uid] = {};
}
if (!handlers[uid][eventName]) {
handlers[uid][eventName] = [];
var existingHandler = element['on' + eventName];
if (existingHandler) {
handlers[uid][eventName].push(existingHandler);
}
element['on' + eventName] = createDispatcher(uid, eventName);
}
handlers[uid][eventName].push(handler);
};
removeListener = function(element, eventName, handler) {
var uid = getUniqueId(element);
if (handlers[uid] && handlers[uid][eventName]) {
var handlersForEvent = handlers[uid][eventName];
for (var i = 0, len = handlersForEvent.length; i < len; i++) {
if (handlersForEvent[i] === handler){
handlersForEvent.splice(i, 1);
}
}
}
};
}
global.addListener = addListener;
global.removeListener = removeListener;
})(this);

至此.我们的整个事件函数算是发展到了比较完美的地步.但总归还是有我们没照顾到的地方.只能惊叹IE和w3c对于事件的处理相差太大了.
遗漏的细节
尽管我们洋洋洒洒的上百行代码修正了一个兼容的事件机制.但仍然有需要完善的地方.
1. 由于MSHTML DOM不支持事件机制不支持捕获阶段.所以第三个参数就让他缺失去吧.
2. 事件句柄触发顺序.大多数浏览器都是FIFO(先进先出).而IE偏偏就要来个LIFO(后进先出).其实DOM3草案已经说明了specifies the order as FIFO.
其他细节不一一道来.

整个文章为了记录自己的思路.所以显得比较啰嗦.但那个相对完美的事件函数还是有稍许参考价值, 希望会对大家有稍许帮助.

如果大家有好的意见和提议,望指教.谢谢.
代码打包下载

(0)

相关推荐

  • js跨浏览器的事件侦听器和事件对象的使用方法

    本文特意为跨浏览器实现添加事件侦听器和跨浏览器事件对象的使用方法做了下总结,并把这些方法打包,欢迎大家学习. 打包的一个EventUtil对象 var EventUtil = { // 添加侦听事件 addEventListener:function (element, type, handler) { // IE9+.Firefox.Safari.chrome和Opera if(element.addEventListener) { element.addEventListener(type,

  • 详解javascript跨浏览器事件处理程序

    本文为大家分享了javascript跨浏览器事件处理机制,供大家参考,具体内容如下 <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>跨浏览器的事件处理程序</title> </head> <body> <input type="button" value=

  • javascript 跨浏览器开发经验总结(五) js 事件

    简单事件模型和高级事件模型 简单事件模型和高级事件模型简单事件,就是事件与页面元素直观的绑定在一起的形式,如: 复制代码 代码如下: <div onclick="alert(this.innerHTML);"> element.onclick = function(){alert(this.innerHTML);} 只要不是用了个别浏览器独有的事件,一般的click,mouseover事件等在各浏览器中都可以这么使用. 但是当一个事件需要绑定多个监听,或者需要动态注册/移出

  • js事件处理程序跨浏览器解决方案

    本文实例为大家分享了js事件处理程序跨浏览器解决方案,供大家参考,具体内容如下 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title></title> </head> <body> <div> <input type="button" id="button1" va

  • myEvent.js javascript跨浏览器事件框架

    event究竟有多么复杂?可见前辈的6年前的努力:最佳的addEvent是怎样诞生的,后起之秀jQuery也付出了一千六百多行血汗代码(v 1.5.1)搞定了6年后出现的各种核的浏览器. 我参考前辈的代码以及自己的理解尝试写了一个事件框架,我的框架完成了一个事件机制的核心,它能提供统一接口实现多事件绑定以及避免内存泄漏等其他一些问题,更重要的是性能还不错. 我的手法: 所有回调函数根据元素.事件类型.回调函数唯一ID缓存在一个_create对象中(其内部具体结构可见下面源码的关于_cache的注

  • javascript 跨浏览器的事件系统

    但实质上javascript之父也不能主宰这一切,他支持的网景也没有强大到让竞争对手乖乖地使用它的产品,微软搞了一个JScript,死去的Macromedia 搞了一个ActionScript,还有更多,听说这个分支挺复杂的.但借用浏览器内置的DOM事件模型,第一个后果是,想使用它就必须借助某个DOM对象,window,document或元素节点,第二个后果是由于每个浏览器对DOM的支持不一,不能确保事件模型的一致,第三个是由于基于DOM对象,很容易造成循环引用.微软打赢第一次浏览器战争后,就基

  • javascript高级程序设计第二版第十二章事件要点总结(常用的跨浏览器检测方法)

    复制代码 代码如下: var EventUtil={ //跨浏览器处理程序---创建方法 addHandler:function(element,type,handler){ if(element.addEventListener){ element.addEventListneter(type,handler,false); }else if(element.attachEvent){ element.attachEvent("on"+type,handler); }else{ el

  • 详细解读JavaScript的跨浏览器事件处理

    一.关于获取事件对象 FF有点倔强,只支持arguments[0],不支持window.event.这次真的不怪IE,虽然把event作为window的属性不合规范,但大家都已经默许这个小问题存在了,只有FF这么多年了还是特立独行.所以,跨浏览器的事件对象获取有以下两种方式: 带参的: getEvent : function(event){ return event ? event : window.event; //return event || window.event;//或者更简单的方式

  • JavaScript中的跨浏览器事件操作的基本方法整理

    绑定事件 EU.addHandler = function(element,type,handler){ //DOM2级事件处理,IE9也支持 if(element.addEventListener){ element.addEventListener(type,handler,false); } else if(element.attachEvent){ //type加'on' //IE9也可以这样绑定 element.attachEvent('on' + type,handler); } /

  • JavaScript实现跨浏览器的添加及删除事件绑定函数实例

    本文实例讲述了JavaScript实现跨浏览器的添加及删除事件绑定函数.分享给大家供大家参考.具体如下: IE 的事件绑定函数是 attachEvent:而 Firefox, Safari 是 addEventListener:Opera 则两种都支持.使用jQuery就可以使用简单的bind(),或者$().click()之类的函数解决,而如果不使用JavaScript框架的时候,大家可是使用下面的封装bind()函数. 添加事件绑定 bind() /*********************

随机推荐