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获取CSS样式中的颜色值的问题,不同浏览器格式不同的解决办法
May 13 Javascript
JS获取url链接字符串 location.href
Dec 23 Javascript
Jquery解析Json格式数据过程代码
Oct 17 Javascript
js父页面中使用子页面的方法
Jan 09 Javascript
微信js-sdk预览图片接口及从拍照或手机相册中选图接口用法示例
Oct 13 Javascript
微信小程序  Mustache语法详细介绍
Oct 27 Javascript
JS重载实现方法分析
Dec 16 Javascript
jQuery插件HighCharts绘制的2D堆柱状图效果示例【附demo源码下载】
Mar 14 Javascript
Vue实现搜索 和新闻列表功能简单范例
Mar 16 Javascript
详解vue-cli 脚手架 安装
Apr 16 Javascript
在layui下对元素进行事件绑定的实例
Sep 06 Javascript
关于angular引入ng-zorro的问题浅析
Sep 09 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写的采集程序
2007/03/16 PHP
php下MYSQL limit的优化
2008/01/10 PHP
PHP 文章中的远程图片采集到本地的代码
2009/07/30 PHP
PHP的foreach中使用引用时需要注意的一个问题和解决方法
2014/05/29 PHP
ThinkPHP模板判断输出Defined标签用法详解
2014/06/30 PHP
Codeigniter(CI)框架分页函数及相关知识
2014/11/03 PHP
Linux系统下PHP-FPM的安装和配置教程
2015/08/17 PHP
ThinkPHP框架实现的邮箱激活功能示例
2018/06/15 PHP
很酷的javascript loading效果代码
2008/06/18 Javascript
JQuery Tips(3) 关于$()包装集内元素的改变
2009/12/14 Javascript
javascript 设计模式之单体模式 面向对象学习基础
2010/04/18 Javascript
拉动滚动条加载数据的jquery代码
2012/05/03 Javascript
js获取当前日期代码适用于网页头部
2013/06/27 Javascript
javascript 终止函数执行操作
2014/02/14 Javascript
最新最热最实用的15个jQuery插件汇总
2015/07/05 Javascript
详解React-Todos入门例子
2016/11/08 Javascript
JavaScript自动点击链接 防止绕过浏览器访问的方法
2017/01/19 Javascript
Bootstrap常用组件学习(整理)
2017/03/24 Javascript
vue 文件目录结构详解
2017/11/24 Javascript
nodejs基础之常用工具模块util用法分析
2018/12/26 NodeJs
详解nodejs 开发企业微信第三方应用入门教程
2019/03/12 NodeJs
[02:51]2014DOTA2国际邀请赛 IG战队官方纪录片
2014/07/21 DOTA
Python中实现参数类型检查的简单方法
2015/04/21 Python
讲解Python中的标识运算符
2015/05/14 Python
Python中的anydbm模版和shelve模版使用指南
2015/07/09 Python
详解python单元测试框架unittest
2018/07/02 Python
Python 中 function(#) (X)格式 和 (#)在Python3.*中的注意事项
2018/11/30 Python
基于Python中isfile函数和isdir函数使用详解
2019/11/29 Python
css3实现波纹特效、H5实现动态波浪效果
2018/01/31 HTML / CSS
日本网路线上商品代购服务:转送JAPAN
2016/08/05 全球购物
介绍一下EJB的体系结构
2012/08/01 面试题
12岁生日感言
2014/01/21 职场文书
个人反四风对照检查材料思想汇报
2014/09/23 职场文书
个人整改措施落实情况汇报
2014/10/29 职场文书
六五普法心得体会2016
2016/01/21 职场文书
Spring Security动态权限的实现方法详解
2022/06/16 Java/Android