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 相关文章推荐
JavaScript中Math对象使用说明
Jan 16 Javascript
超酷的网页音乐播放器DewPlayer使用方法
Dec 18 Javascript
JavaScript 中的日期和时间及表示标准介绍
Aug 21 Javascript
js使用DOM设置单选按钮、复选框及下拉菜单的方法
Jan 20 Javascript
JQuery和PHP结合实现动态进度条上传显示
Nov 23 Javascript
Angular.JS通过指令操作DOM的方法
May 10 Javascript
Node.js命令行/批处理中如何更改Linux用户密码浅析
Jul 22 Javascript
详解微信小程序-canvas绘制文字实现自动换行
Apr 26 Javascript
javascript function(函数类型)使用与注意事项小结
Jun 10 Javascript
JS实现点星星消除小游戏
Mar 24 Javascript
深入了解JavaScript词法作用域
Jul 29 Javascript
Vue+Bootstrap实现简易学生管理系统
Feb 09 Vue.js
最佳的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
文件上传的实现
2006/10/09 PHP
php5.4以下版本json不支持不转义内容中文的解决方法
2015/01/13 PHP
php实现字符串首字母大写和单词首字母大写的方法
2015/03/14 PHP
PHP实现通过URL提取根域名
2016/03/31 PHP
Yii核心验证器api详解
2016/11/23 PHP
微信第三方登录(原生)demo【必看篇】
2017/05/26 PHP
PHP正则匹配到2个字符串之间的内容方法
2018/12/24 PHP
PHP大文件分块上传功能实例详解
2019/07/22 PHP
基于PHP实现堆排序原理及实例详解
2020/06/19 PHP
JS控制阿拉伯数字转为中文大写示例代码
2013/09/04 Javascript
js中string转int把String类型转化成int类型
2014/08/13 Javascript
原创jQuery弹出层插件分享
2015/04/02 Javascript
jQuery实现的图文高亮滚动切换特效实例
2015/08/10 Javascript
Bootstrap每天必学之js插件
2015/11/30 Javascript
JS中事件冒泡和事件捕获介绍
2016/12/13 Javascript
BootStrap select2 动态改变值的方法
2017/02/10 Javascript
重新理解JavaScript的六种继承方式
2017/03/24 Javascript
node.js中cluster的使用教程
2017/06/09 Javascript
Vue自定义事件(详解)
2017/08/19 Javascript
JS验证输入的是否是数字及保留几位小数问题
2018/05/09 Javascript
es6中比较有用的7个技巧小结
2019/07/12 Javascript
layui实现左侧菜单点击右侧内容区显示
2019/07/26 Javascript
基于layui内置模块(element常用元素的操作)
2019/09/20 Javascript
简单了解JavaScript arguement原理及作用
2020/05/28 Javascript
JS操作JSON常用方法(10w阅读)
2020/12/06 Javascript
vue实现两个区域滚动条同步滚动
2020/12/13 Vue.js
简单谈谈Python中的几种常见的数据类型
2017/02/10 Python
Python使用wxPython实现计算器
2018/01/30 Python
对python 树状嵌套结构的实现思路详解
2019/08/09 Python
Python PyQt5运行程序把输出信息展示到GUI图形界面上
2020/04/27 Python
Python selenium 加载并保存QQ群成员,去除其群主、管理员信息的示例代码
2020/05/28 Python
html5中的一些标签学习(心得)
2016/10/18 HTML / CSS
高中美术教学反思
2014/01/19 职场文书
保密工作责任书
2014/04/16 职场文书
农民工预备党员思想汇报
2014/09/14 职场文书
基层党员四风问题自我剖析材料
2014/09/29 职场文书