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 相关文章推荐
使用jQuery.Validate进行客户端验证(初级篇) 不使用微软验证控件的理由
Jun 28 Javascript
基于jQuery制作迷你背词汇工具
Jul 27 Javascript
jQuery实现切换页面过渡动画效果
Oct 29 Javascript
微信小程序 封装http请求实例详解
Jan 16 Javascript
基于Bootstrap分页的实例讲解(必看篇)
Jul 04 Javascript
JQuery判断正整数整理小结
Aug 21 jQuery
基于Cookie常用操作以及属性介绍
Sep 07 Javascript
XMLHttpRequest对象_Ajax异步请求重点(推荐)
Sep 28 Javascript
移动前端图片压缩上传的实例
Dec 06 Javascript
jQuery实现右侧抽屉式在线客服功能
Dec 25 jQuery
详解webpack中的hash、chunkhash、contenthash区别
Jan 05 Javascript
javascript实现超好看的3D烟花特效
Jan 01 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设计模式 Facade(外观模式)
2011/06/26 PHP
php使用pack处理二进制文件的方法
2014/07/03 PHP
php的4种常见运行方式
2015/03/20 PHP
PHP自定义函数获取汉字首字母的方法
2016/12/01 PHP
超强多功能php绿色集成环境详解
2017/01/25 PHP
javascript 建设银行登陆键盘
2008/06/10 Javascript
JQuery与JS里submit()的区别示例介绍
2014/02/17 Javascript
使用javascript实现监控视频播放并打印日志
2015/01/05 Javascript
C#中使用迭代器处理等待任务
2015/07/13 Javascript
jquery datatable服务端分页
2016/08/31 Javascript
AngularJS实现网站换肤实例
2021/02/19 Javascript
jQuery实现Select下拉列表进行状态选择功能
2017/03/30 jQuery
JS实现批量上传文件并显示进度功能
2017/06/27 Javascript
JS通过调用微信API实现微信支付功能的方法示例
2017/06/29 Javascript
详解基于vue-cli3快速发布一个fullpage组件
2019/03/08 Javascript
vue自定义插件封装,实现简易的elementUi的Message和MessageBox的示例
2020/11/20 Vue.js
JS removeAttribute()方法实现删除元素的某个属性
2021/01/11 Javascript
[06:07]辉夜杯现场观众互动 “比谁远送显示器”
2015/12/26 DOTA
python ip正则式
2009/05/07 Python
Python下singleton模式的实现方法
2014/07/16 Python
Python中type的构造函数参数含义说明
2015/06/21 Python
python 解决动态的定义变量名,并给其赋值的方法(大数据处理)
2018/11/10 Python
Python解决线性代数问题之矩阵的初等变换方法
2018/12/12 Python
Python3基本输入与输出操作实例分析
2020/02/14 Python
使用Keras中的ImageDataGenerator进行批次读图方式
2020/06/17 Python
scrapy框架携带cookie访问淘宝购物车功能的实现代码
2020/07/07 Python
python regex库实例用法总结
2021/01/03 Python
PyCharm+Miniconda3安装配置教程详解
2021/02/16 Python
详解H5 活动页之移动端 REM 布局适配方法
2017/12/07 HTML / CSS
巴塞罗那观光通票:Barcelona Pass
2019/10/30 全球购物
社区学雷锋活动策划方案
2014/01/30 职场文书
园艺师求职信
2014/04/27 职场文书
合作协议书模板
2014/10/10 职场文书
党校学习党性分析材料
2014/12/19 职场文书
专职安全员岗位职责
2015/04/11 职场文书
mysql的单列多值存储实例详解
2022/04/05 MySQL