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 相关文章推荐
XML+XSL 与 HTML 两种方案的结合
Apr 22 Javascript
模拟多级复选框效果的jquery代码
Aug 13 Javascript
jQuery源码解读之removeClass()方法分析
Feb 20 Javascript
实例代码详解jquery.slides.js
Nov 16 Javascript
让html元素随浏览器的大小自适应垂直居中的实现方法
Oct 12 Javascript
简单谈谈ES6的六个小特性
Nov 18 Javascript
bootstrap学习使用(导航条、下拉菜单、轮播、栅格布局等)
Dec 01 Javascript
JS实现的添加弹出层并完成锁屏操作示例
Apr 07 Javascript
JS实现异步上传压缩图片
Apr 22 Javascript
jQuery+ajax实现动态添加表格tr td功能示例
Apr 23 jQuery
JS实现匀速与减速缓慢运动的动画效果封装示例
Aug 27 Javascript
详解JavaScript 浮点数运算的精度问题
Jul 23 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
《PHP编程最快明白》第六讲:Mysql数据库操作
2010/11/01 PHP
PHP实用函数分享之去除多余的0
2015/02/06 PHP
php使用Jpgraph绘制饼状图的方法
2015/06/10 PHP
PHP Static延迟静态绑定用法分析
2016/03/16 PHP
图文详解phpstorm配置Xdebug进行调试PHP教程
2016/06/13 PHP
javascript indexOf函数使用说明
2008/07/03 Javascript
jquery.cookie.js 操作cookie实现记住密码功能的实现代码
2011/04/27 Javascript
利用JS延迟加载百度分享代码,提高网页速度
2013/07/01 Javascript
javascript简单实现等比例缩小图片的方法
2016/07/27 Javascript
JavaScript面向对象编写购物车功能
2016/08/19 Javascript
jquery滚动条插件slimScroll使用方法
2017/02/09 Javascript
微信小程序 下拉列表的实现实例代码
2017/03/08 Javascript
利用JavaScript对中文(汉字)进行排序实例详解
2017/06/18 Javascript
Vue快速实现通用表单验证功能
2019/12/05 Javascript
python模拟登陆阿里妈妈生成商品推广链接
2014/04/03 Python
轻松掌握python设计模式之策略模式
2016/11/18 Python
Python 十六进制整数与ASCii编码字符串相互转换方法
2018/07/09 Python
python 返回一个列表中第二大的数方法
2019/07/09 Python
详解Python中正则匹配TAB及空格的小技巧
2019/07/26 Python
django框架单表操作之增删改实例分析
2019/12/16 Python
Python基础教程(一)——Windows搭建开发Python开发环境
2020/07/20 Python
Myprotein比利时官方网站:欧洲第一运动营养品牌
2020/10/04 全球购物
幼儿园园长自我鉴定
2013/10/22 职场文书
污水厂厂长岗位职责
2014/01/04 职场文书
摄影助理岗位职责
2014/02/07 职场文书
优秀辅导员事迹材料
2014/02/16 职场文书
如何写求职信
2014/05/24 职场文书
博士生导师推荐信
2014/07/08 职场文书
知识就是力量演讲稿
2014/09/13 职场文书
初中生旷课检讨书范文
2014/10/06 职场文书
单位考核聘任报告
2015/03/02 职场文书
行政上诉状范文
2015/05/23 职场文书
学生会副主席竞选稿
2015/11/19 职场文书
详解Python牛顿插值法
2021/05/11 Python
Axios代理配置及封装响应拦截处理方式
2022/04/07 Vue.js
Python实现聚类K-means算法详解
2022/07/15 Python