myEvent.js 轻量级跨浏览器事件框架实现与扩展

作者:小圆点

时间:2026-10-27

浏览:120

myEvent.js 是一个轻量级的跨浏览器 JavaScript 事件框架,旨在解决多浏览器兼容性问题并优化性能。该框架通过统一接口实现多事件绑定,利用缓存机制避免内存泄漏,并支持自定义事件扩展。

文章目录

跨浏览器事件框架概述

事件处理的复杂性可见一斑,前辈在六年前就致力于探索最佳的 addEvent 实现方式。即便后起之秀 jQuery 也投入了大量精力,其 v 1.5.1 版本包含了一千六百多行代码,才解决了随后出现的各种多核浏览器兼容问题。



基于对前辈代码的参考及个人理解,我尝试编写了一个事件框架。该框架实现了事件机制的核心功能,提供统一接口以支持多事件绑定,并有效避免了内存泄漏等常见问题,同时保持了不错的运行性能。



我的手法:

所有回调函数均根据元素、事件类型及回调函数的唯一 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)

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章