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


Posted in Javascript onOctober 24, 2011

event究竟有多么复杂?可见前辈的6年前的努力:最佳的addEvent是怎样诞生的,后起之秀jQuery也付出了一千六百多行血汗代码(v 1.5.1)搞定了6年后出现的各种核的浏览器。

我参考前辈的代码以及自己的理解尝试写了一个事件框架,我的框架完成了一个事件机制的核心,它能提供统一接口实现多事件绑定以及避免内存泄漏等其他一些问题,更重要的是性能还不错。

我的手法:

所有回调函数根据元素、事件类型、回调函数唯一ID缓存在一个_create对象中(其内部具体结构可见下面源码的关于_cache的注释)。
事件绑定使用一个_create代理函数处理,并且一个元素的各类型事件全部通过此进行分发,同时运用apply方法让IE的指针指向元素。
通过数组队列解决IE回调函数执行顺序的问题。
fix函数将处理回调函数传入的event参数以及其他兼容问题。此处参考了jQuery.event.fix。
断开事件与元素的循环引用避免内存泄漏。
一、核心实现:

// myEvent 0.2 
// 2011.04.06 - TangBin - planeart.cn - MIT Licensed 
/** 
* 事件框架 
* @namespace 
* @see http://www.planeart.cn/?p=1285 
*/ 
var myEvent = (function () { 
var _fid = 1, 
_guid = 1, 
_time = (new Date).getTime(), 
_nEid = '{$eid}' + _time, 
_nFid = '{$fid}' + _time, 
_DOM = document.addEventListener, 
_noop = function () {}, 
_create = function (guid) { 
return function (event) { 
event = api.fix(event || window.event); 
var i = 0, 
type = (event || (event = document.event)).type, 
elem = _cache[guid].elem, 
data = arguments, 
events = _cache[guid].events[type]; 
for (; i < events.length; i ++) { 
if (events[i].apply(elem, data) === false) event.preventDefault(); 
}; 
}; 
}, 
_cache = {/* 
1: { 
elem: (HTMLElement), 
events: { 
click: [(Function), (..)], 
(..) 
}, 
listener: (Function) 
}, 
(..) 
*/}; 
var api = { 
/** 
* 事件绑定 
* @param {HTMLElement} 元素 
* @param {String} 事件名 
* @param {Function} 要绑定的函数 
*/ 
bind: function (elem, type, callback) { 
var guid = elem[_nEid] || (elem[_nEid] = _guid ++); 
if (!_cache[guid]) _cache[guid] = { 
elem: elem, 
listener: _create(guid), 
events: {} 
}; 
if (type && !_cache[guid].events[type]) { 
_cache[guid].events[type] = []; 
api.add(elem, type, _cache[guid].listener); 
}; 
if (callback) { 
if (!callback[_nFid]) callback[_nFid] = _fid ++; 
_cache[guid].events[type].push(callback); 
} else 
return _cache[guid]; 
}, 
/** 
* 事件卸载 
* @param {HTMLElement} 元素 
* @param {String} 事件名 
* @param {Function} 要卸载的函数 
*/ 
unbind: function (elem, type, callback) { 
var events, i, list, 
guid = elem[_nEid], 
handler = _cache[guid]; 
if (!handler) return; 
events = handler.events; 
if (callback) { 
list = events[type]; 
if (!list) return; 
for (i = 0; i < list.length; i ++) { 
list[i][_nFid] === callback[_nFid] && list.splice(i--, 1); 
}; 
if (list.length === 0) return api.unbind(elem, type); 
} else if (type) { 
delete events[type]; 
api.remove(elem, type, handler.listener); 
} else { 
for (i in events) { 
api.remove(elem, i, handler.listener); 
}; 
delete _cache[guid]; 
}; 
}, 
/** 
* 事件触发 (注意:不会触发浏览器默认行为与冒泡) 
* @param {HTMLElement} 元素 
* @param {String} 事件名 
* @param {Array} (可选)附加数据 
*/ 
triggerHandler: function (elem, type, data) { 
var guid = elem[_nEid], 
event = { 
type: type, 
target: elem, 
currentTarget: elem, 
preventDefault: _noop, 
stopPropagation: _noop 
}; 
data = data || []; 
data.unshift(event); 
guid && _cache[guid].listener.apply(elem, data); 
try { 
elem['on' + type] && elem['on' + type].apply(elem, data); 
//elem[type] && elem[type](); 
} catch (e) {}; 
}, 
// 原生事件绑定接口 
add: _DOM ? function (elem, type, listener) { 
elem.addEventListener(type, listener, false); 
} : function (elem, type, listener) { 
elem.attachEvent('on' + type, listener); 
}, 
// 原生事件卸载接口 
remove: _DOM ? function (elem, type, listener) { 
elem.removeEventListener(type, listener, false); 
} : function (elem, type, listener) { 
elem.detachEvent('on' + type, listener); 
}, 
// 修正 
fix: function (event) { 
if (_DOM) return event; 
var name, 
newEvent = {}, 
doc = document.documentElement, 
body = document.body; 
newEvent.target = event.srcElement || document; 
newEvent.target.nodeType === 3 && (newEvent.target = newEvent.target.parentNode); 
newEvent.preventDefault = function () {event.returnValue = false}; 
newEvent.stopPropagation = function () {event.cancelBubble = true}; 
newEvent.pageX = newEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); 
newEvent.pageY = newEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); 
newEvent.relatedTarget = event.fromElement === newEvent.target ? event.toElement : event.fromElement; 
// !!IE写event会极其容易导致内存泄漏,Firefox写event会报错 
// 拷贝event 
for (name in event) newEvent[name] = event[name]; 
return newEvent; 
} 
}; 
return api; 
})();

我给一万个元素绑定事件进行了测试,测试工具为sIEve,结果:
事件框架 耗时 内存
IE8
jQuery.bind 1064 MS 79.80 MB
myEvent.bind 623 MS 35.82 MB
IE6
jQuery.bind 2503 MS 74.23 MB
myEvent.bind 1810 MS 28.48 MB

可以看到无论是执行效率还是内存占用myEvent都比有一定优势,这是可能是由于jQuery事件机制过于强大导致其性能的损耗。
测试样本:http://www.planeart.cn/demo/myEvent/

二、扩展自定义事件机制
jQuery是可以自定义事件的,它用一个special命名空间存储自定义事件,我在上面代码的基础上模仿jQuery自定义事件机制,并把其著名的ready事件与另外一个jQuery hashchange事件插件移植过来。

这两个自定义事件非常重要,ready事件可以在DOM就绪给元素绑定事件,比传统使用window.onload要快很多;hashchange事件可以监听锚点改变,常用于解决AJAX历史记录问题,如Twitter新版本就就采用此处理AJAX,使用锚点机制除了可以提高AJAX应用程序的用户体验外,如果按照一定规则还能被google索引到。

当然,我前面文章实现的imgReady事件也可以通过此扩展进来,稍后更新。

// myEvent 0.2.2 
// 2011.04.07 - TangBin - planeart.cn - MIT Licensed 
/** 
* 事件框架 
* @namespace 
* @see http://www.planeart.cn/?p=1285 
*/ 
var myEvent = (function () { 
var _ret, _name, 
_fid = 1, 
_guid = 1, 
_time = (new Date).getTime(), 
_nEid = '{$eid}' + _time, 
_nFid = '{$fid}' + _time, 
_DOM = document.addEventListener, 
_noop = function () {}, 
_create = function (guid) { 
return function (event) { 
event = myEvent.fix(event || window.event); 
var type = (event || (event = document.event)).type, 
elem = _cache[guid].elem, 
data = arguments, 
events = _cache[guid].events[type], 
i = 0, 
length = events.length; 
for (; i < length; i ++) { 
if (events[i].apply(elem, data) === false) event.preventDefault(); 
}; 
event = elem = null; 
}; 
}, 
_cache = {/* 
1: { 
elem: (HTMLElement), 
events: { 
click: [(Function), (..)], 
(..) 
}, 
listener: (Function) 
}, 
(..) 
*/}; 
var API = function () {}; 
API.prototype = { 
/** 
* 事件绑定 
* @param {HTMLElement} 元素 
* @param {String} 事件名 
* @param {Function} 要绑定的函数 
*/ 
bind: function (elem, type, callback) { 
var events, listener, 
guid = elem[_nEid] || (elem[_nEid] = _guid ++), 
special = this.special[type] || {}, 
cacheData = _cache[guid]; 
if (!cacheData) cacheData = _cache[guid] = { 
elem: elem, 
listener: _create(guid), 
events: {} 
}; 
events = cacheData.events; 
listener = cacheData.listener; 
if (!events[type]) events[type] = []; 
if (!callback[_nFid]) callback[_nFid] = _fid ++; 
if (!special.setup || special.setup.call(elem, listener) === false) { 
events[type].length === 0 && this.add(elem, type, listener); 
}; 
events[type].push(callback); 
}, 
/** 
* 事件卸载 
* @param {HTMLElement} 元素 
* @param {String} 事件名 
* @param {Function} 要卸载的函数 
*/ 
unbind: function (elem, type, callback) { 
var events, special, i, list, fid, 
guid = elem[_nEid], 
cacheData = _cache[guid]; 
if (!cacheData) return; 
events = cacheData.events; 
if (callback) { 
list = events[type]; 
fid = callback[_nFid]; 
if (!list) return; 
for (i = 0; i < list.length; i ++) { 
list[i][_nFid] === fid && list.splice(i--, 1); 
}; 
if (!list.length) this.unbind(elem, type); 
} else if (type) { 
special = this.special[type] || {}; 
if (!special.teardown || special.teardown.call(elem) === false) { 
this.remove(elem, type, cacheData.listener); 
}; 
delete events[type]; 
} else { 
for (i in events) { 
this.remove(elem, i, cacheData.listener); 
}; 
delete _cache[guid]; 
}; 
}, 
/** 
* 事件触发 (注意:不会触发浏览器默认行为与冒泡) 
* @param {HTMLElement} 元素 
* @param {String} 事件名 
* @param {Array} (可选)附加数据 
*/ 
triggerHandler: function (elem, type, data) { 
var guid = elem[_nEid], 
cacheData = _cache[guid], 
event = { 
type: type, 
target: elem, 
currentTarget: elem, 
preventDefault: _noop, 
stopPropagation: _noop 
}; 
data = data || []; 
data.unshift(event); 
cacheData && cacheData.events[type] && _cache[guid].listener.apply(elem, data); 
try { 
elem['on' + type] && elem['on' + type].apply(elem, data); 
//elem[type] && elem[type](); 
} catch (e) {}; 
}, 
// 自定义事件接口 
special: {}, 
// 原生事件绑定接口 
add: _DOM ? function (elem, type, listener) { 
elem.addEventListener(type, listener, false); 
} : function (elem, type, listener) { 
elem.attachEvent('on' + type, listener); 
}, 
// 原生事件卸载接口 
remove: _DOM ? function (elem, type, listener) { 
elem.removeEventListener(type, listener, false); 
} : function (elem, type, listener) { 
elem.detachEvent('on' + type, listener); 
}, 
// 修正 
fix: function (event) { 
if (_DOM) return event; 
var name, 
newEvent = {}, 
doc = document.documentElement, 
body = document.body; 
newEvent.target = event.srcElement || document; 
newEvent.target.nodeType === 3 && (newEvent.target = newEvent.target.parentNode); 
newEvent.preventDefault = function () {event.returnValue = false}; 
newEvent.stopPropagation = function () {event.cancelBubble = true}; 
newEvent.pageX = newEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); 
newEvent.pageY = newEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); 
newEvent.relatedTarget = event.fromElement === newEvent.target ? event.toElement : event.fromElement; 
// !!直接写event IE导致内存泄漏,Firefox会报错 
// 伪装event 
for (name in event) newEvent[name] = event[name]; 
return newEvent; 
} 
}; 
return new API(); 
})(); 
// DOM就绪事件 
myEvent.ready = (function () { 
var readyList = [], DOMContentLoaded, 
readyBound = false, isReady = false; 
function ready () { 
if (!isReady) { 
if (!document.body) return setTimeout(ready, 13); 
isReady = true; 
if (readyList) { 
var fn, i = 0; 
while ((fn = readyList[i++])) { 
fn.call(document, {}); 
}; 
readyList = null; 
}; 
}; 
}; 
function bindReady () { 
if (readyBound) return; 
readyBound = true; 
if (document.readyState === 'complete') { 
return ready(); 
}; 
if (document.addEventListener) { 
document.addEventListener('DOMContentLoaded', DOMContentLoaded, false); 
window.addEventListener('load', ready, false); 
} else if (document.attachEvent) { 
document.attachEvent('onreadystatechange', DOMContentLoaded); 
window.attachEvent('onload', ready); 
var toplevel = false; 
try { 
toplevel = window.frameElement == null; 
} catch (e) {}; 
if (document.documentElement.doScroll && toplevel) { 
doScrollCheck(); 
}; 
}; 
}; 
myEvent.special.ready = { 
setup: bindReady, 
teardown: function () {} 
}; 
if (document.addEventListener) { 
DOMContentLoaded = function () { 
document.removeEventListener('DOMContentLoaded', DOMContentLoaded, false); 
ready(); 
}; 
} else if (document.attachEvent) { 
DOMContentLoaded = function () { 
if (document.readyState === 'complete') { 
document.detachEvent('onreadystatechange', DOMContentLoaded); 
ready(); 
}; 
}; 
}; 
function doScrollCheck () { 
if (isReady) return; 
try { 
document.documentElement.doScroll('left'); 
} catch (e) { 
setTimeout(doScrollCheck, 1); 
return; 
}; 
ready(); 
}; 
return function (callback) { 
bindReady(); 
if (isReady) { 
callback.call(document, {}); 
} else if (readyList) { 
readyList.push(callback); 
}; 
return this; 
}; 
})(); 
// Hashchange Event v1.3 
(function (window, undefined) { 
var config = { 
delay: 50, 
src: null, 
domain: null 
}, 
str_hashchange = 'hashchange', 
doc = document, 
isIE = !-[1,], 
fake_onhashchange, special = myEvent.special, 
doc_mode = doc.documentMode, 
supports_onhashchange = 'on' + str_hashchange in window && (doc_mode === undefined || doc_mode > 7); 
function get_fragment(url) { 
url = url || location.href; 
return '#' + url.replace(/^[^#]*#?(.*)$/, '$1'); 
}; 
special[str_hashchange] = { 
setup: function () { 
if (supports_onhashchange) return false; 
myEvent.ready(fake_onhashchange.start); 
}, 
teardown: function () { 
if (supports_onhashchange) return false; 
myEvent.ready(fake_onhashchange.stop); 
} 
}; 
/** @inner */ 
fake_onhashchange = (function () { 
var self = {}, 
timeout_id, last_hash = get_fragment(), 
/** @inner */ 
fn_retval = function (val) { 
return val; 
}, 
history_set = fn_retval, 
history_get = fn_retval; 
self.start = function () { 
timeout_id || poll(); 
}; 
self.stop = function () { 
timeout_id && clearTimeout(timeout_id); 
timeout_id = undefined; 
}; 
function poll() { 
var hash = get_fragment(), 
history_hash = history_get(last_hash); 
if (hash !== last_hash) { 
history_set(last_hash = hash, history_hash); 
myEvent.triggerHandler(window, str_hashchange); 
} else if (history_hash !== last_hash) { 
location.href = location.href.replace(/#.*/, '') + history_hash; 
}; 
timeout_id = setTimeout(poll, config.delay); 
}; 
isIE && !supports_onhashchange && (function () { 
var iframe,iframe_src, iframe_window; 
self.start = function () { 
if (!iframe) { 
iframe_src = config.src; 
iframe_src = iframe_src && iframe_src + get_fragment(); 
iframe = doc.createElement('<IFRAME title=empty style="DISPLAY: none" tabIndex=-1 src="' + (iframe_src || 'javascript:0') + '"></IFRAME>'); 
myEvent.bind(iframe, 'load', function () { 
myEvent.unbind(iframe, 'load'); 
iframe_src || history_set(get_fragment()); 
poll(); 
}); 
doc.getElementsByTagName('html')[0].appendChild(iframe); 
iframe_window = iframe.contentWindow; 
doc.onpropertychange = function () { 
try { 
if (event.propertyName === 'title') { 
iframe_window.document.title = doc.title; 
}; 
} catch (e) {}; 
}; 
}; 
}; 
self.stop = fn_retval; 
/** @inner */ 
history_get = function () { 
return get_fragment(iframe_window.location.href); 
}; 
/** @inner */ 
history_set = function (hash, history_hash) { 
var iframe_doc = iframe_window.document, 
domain = config.domain; 
if (hash !== history_hash) { 
iframe_doc.title = doc.title; 
iframe_doc.open(); 
domain && iframe_doc.write('<SCRIPT>document.domain="' + domain + '"</SCRIPT>'); 
iframe_doc.close(); 
iframe_window.location.hash = hash; 
}; 
}; 
})(); 
return self; 
})(); 
})(this);

ready事件是伪事件,调用方式:
myEvent.ready(function () { 
//[code..] 
});

hashchange事件可以采用标准方式绑定:
myEvent.bind(window, 'hashchange', function () { 
//[code..] 
});

这里有一些文章值得阅读:
javascript 跨浏览器的事件系统(司徒正美。他博客有一系列的讲解)
更优雅的兼容(BELLEVE INVIS)
Javascript 相关文章推荐
用js实现下载远程文件并保存在本地的脚本
May 06 Javascript
深入分析js的冒泡事件
Dec 05 Javascript
JQuery中Ajax()的data参数类型实例分析
Dec 15 Javascript
javascript中FOREACH数组方法使用示例
Mar 01 Javascript
jQuery实现的简单分页示例
Jun 01 Javascript
json定义及jquery操作json的方法
Sep 29 Javascript
NODE.JS跨域问题的完美解决方案
Oct 20 Javascript
用vue和node写的简易购物车实现
Apr 25 Javascript
关于JavaScript的单双引号嵌套问题
Aug 20 Javascript
webpack踩坑之路图片的路径与打包
Sep 05 Javascript
vuejs实现递归树型菜单组件
Jan 13 Javascript
Element InfiniteScroll无限滚动的具体使用方法
Jul 27 Javascript
最佳的addEvent事件绑定是怎样诞生的
Oct 24 #Javascript
关于javascript function对象那些迷惑分析
Oct 24 #Javascript
文本框根据输入内容自适应高度的代码
Oct 24 #Javascript
js创建数据共享接口——简化框架之间相互传值
Oct 23 #Javascript
javascript模版引擎-tmpl的bug修复与性能优化分析
Oct 23 #Javascript
js面向对象设计用{}好还是function(){}好(构造函数)
Oct 23 #Javascript
jQuery EasyUI API 中文文档 - TimeSpinner时间微调器
Oct 23 #Javascript
You might like
phpMyAdmin 安装教程全攻略
2007/03/19 PHP
坏狼的PHP学习教程之第2天
2008/06/15 PHP
php替换超长文本中的特殊字符的函数代码
2012/05/22 PHP
php中并发读写文件冲突的解决方案
2013/10/25 PHP
删除html标签得到纯文本可处理嵌套的标签
2014/04/28 PHP
页面利用渐进式JPEG来提升用户体验度
2014/12/01 PHP
WordPress用户登录框密码的隐藏与部分显示技巧
2015/12/31 PHP
PHP PDOStatement::getColumnMeta讲解
2019/02/01 PHP
php 输出缓冲 Output Control用法实例详解
2020/03/03 PHP
checkbox 复选框不能为空
2009/07/11 Javascript
jquery实用代码片段集合
2010/08/12 Javascript
JQEasy-ui在IE9以下版本中二次加载的问题分析及处理方法
2014/06/23 Javascript
js实现适用于素材网站的黑色多级菜单导航条效果
2015/08/24 Javascript
Node.js的Express框架使用上手指南
2016/03/12 Javascript
js基础之DOM中document对象的常用属性方法详解
2016/10/28 Javascript
vue2.0+ 从插件开发到npm发布的示例代码
2018/04/28 Javascript
详解Vue中的Props与Data细微差别
2020/03/02 Javascript
Python使用Beautiful Soup包编写爬虫时的一些关键点
2016/01/20 Python
Python中字符串的格式化方法小结
2016/05/03 Python
解决csv.writer写入文件有多余的空行问题
2018/07/06 Python
Python实现查找字符串数组最长公共前缀示例
2019/03/27 Python
如何用C代码给Python写扩展库(Cython)
2019/05/17 Python
Pyqt清空某一个QTreeewidgetItem下的所有分支方法
2019/06/17 Python
python爬虫神器Pyppeteer入门及使用
2019/07/13 Python
python中append实例用法总结
2019/07/30 Python
tensorboard实现同时显示训练曲线和测试曲线
2020/01/21 Python
pytorch 移动端部署之helloworld的使用
2020/10/30 Python
python中翻译功能translate模块实现方法
2020/12/17 Python
Monnier Freres中文官网:法国领先的奢侈品配饰在线零售商
2017/11/01 全球购物
Shoes For Crews法国官网:美国领先的防滑鞋设计和制造商
2018/01/01 全球购物
Pam & Gela官网:美国性感前卫女装品牌
2018/07/19 全球购物
公司离职证明范本(5篇)
2014/09/17 职场文书
少年派的奇幻漂流观后感
2015/06/08 职场文书
领导离职感言
2015/08/03 职场文书
党员心得体会范文2016
2016/01/23 职场文书
SQL语法CONSTRAINT约束操作详情
2022/01/18 MySQL