How complex is the event? It can be seen that the efforts of the predecessors 6 years ago: How the best addEvent was born, and the rising star jQuery also spent more than 1,600 lines of hard-earned code (v 1.5.1) to solve the problem that appeared 6 years later Various core browsers.
I tried to write an event framework by referring to the code of my predecessors and my own understanding. My framework completed the core of an event mechanism. It can provide a unified interface to implement multi-event binding, avoid memory leaks, etc. problem, and more importantly the performance is pretty good.
My method:
All callback functions are cached in a _create object based on the element, event type, and unique ID of the callback function (its internal structure can be seen in the source code below Note about _cache).
Event binding is processed using a _create proxy function, and all types of events of an element are distributed through this, and the apply method is used to make IE's pointer point to the element.
Solve the problem of IE callback function execution order through array queue.
The fix function will handle the event parameters passed in the callback function and other compatibility issues. jQuery.event.fix is referenced here.
Disconnect circular references between events and elements to avoid memory leaks.
1. Core implementation:
// myEvent 0.2
// 2011.04.06 - TangBin - planeart.cn - MIT Licensed
/**
* Event frame
* @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 = {
/**
* Event binding
* @param {HTMLElement} element
* @param {String} event name
* @param {Function} function to be bound
*/
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];
},
/**
* Event unloading
* @param {HTMLElement} element
* @param {String} event name
* @param {Function} function to be unloaded
*/
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];
};
},
/**
* Event trigger (note: browser default behavior and bubbling will not be triggered)
* @param {HTMLElement} element
* @param {String} event name
* @param {Array} (Optional) Additional data
*/
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 |
It can be seen that myEvent has certain advantages in terms of execution efficiency and memory usage. This may be due to the loss of performance caused by the jQuery event mechanism being too powerful.
Test sample: http://www.planeart.cn/demo/myEvent/
2. Expand the custom event mechanism
jQuery can customize events. It uses a special namespace to store custom events. I imitated jQuery custom events based on the above code. mechanism, and transplanted its famous ready event and another jQuery hashchange event plug-in.
These two custom events are very important. The ready event can bind events to elements when the DOM is ready, which is much faster than the traditional use of window.onload. The hashchange event can monitor anchor point changes and is often used to solve AJAX history problems. For example, the new version of Twitter uses this method to handle AJAX. In addition to improving the user experience of AJAX applications, using the anchor mechanism can also be indexed by Google if certain rules are followed.
Of course, the imgReady event implemented in my previous article can also be included through this extension and will be updated later.
// myEvent 0.2.2
// 2011.04.07 - TangBin - planeart.cn - MIT Licensed
/**
* Event frame
* @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 = {
/**
* Event binding
* @param {HTMLElement} element
* @param {String} event name
* @param {Function} function to be bound
*/
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);
},
/**
* Event unloading
* @param {HTMLElement} element
* @param {String} event name
* @param {Function} function to be unloaded
*/
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];
};
},
/**
* Event trigger (note: browser default behavior and bubbling will not be triggered)
* @param {HTMLElement} element
* @param {String} event name
* @param {Array} (Optional) Additional data
*/
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('');
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)