Skip to content

Instantly share code, notes, and snippets.

@think49
Last active September 25, 2015 07:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save think49/882821 to your computer and use it in GitHub Desktop.
Save think49/882821 to your computer and use it in GitHub Desktop.
compatible-event.js : addEventListener, attachEvent のラッパー関数。addEvent したリスナーは window unload 時に削除される(循環参照対策)。attachEvent でも実行順が保証される。
/**
* compatible-event.js
*
* @version 0.9.4b
* @author think49
* @url https://gist.github.com/882821
*/
'use strict';
var ListenerEvent = (function () {
'use strict';
if (typeof addEventListener !== 'function' || typeof removeEventListener !== 'function') {
return;
}
/**
* included is-object.js
*
* @version 1.0.3
* @author think49
* @url https://gist.github.com/887049
*/
function isObject (value) {
return value !== null && typeof value !== 'undefined' && Object(value) === value;
}
function ListenerEvent (/*global*/) {
var caches, _global;
if (!(this instanceof ListenerEvent)) {
return new ListenerEvent(arguments[0]);
}
caches = [];
_global = isObject(arguments[0]) ? arguments[0] : Function('return this')();
this.getCacheAll = function getCacheAll () {
return caches;
};
this.setCacheAll = function setCacheAll (inputCaches) {
caches = inputCaches;
};
this.getGlobal = function getGlobal () {
return _global;
};
this.addUnloadEventListener();
}
(function () {
this.add = function add (node, type, listener, useCapture) {
if (type !== 'unload' || node !== this.getGlobal()) {
// console.log([node, type, listener, useCapture]);
node.addEventListener(type, listener, useCapture);
}
this.getCacheAll().push([node, type, listener, useCapture]);
};
this.remove = function remove (node, type, listener, useCapture) {
var cache, caches, i;
node.removeEventListener(type, listener, useCapture);
caches = this.getCacheAll();
i = caches.length;
while (i--) {
cache = caches[i];
if (cache[0] === node && cache[1] === type && cache[2] === listener && cache[3] === useCapture) {
delete caches[i];
this.setCacheAll(caches.slice(0, i).concat(caches.slice(i + 1)));
return;
}
}
};
this.handleEvent = function handleEvent (event) {
var currentTarget, caches, cache, listeners, targetListener, i, j, len1, len2;
currentTarget = event.currentTarget;
caches = this.getCacheAll();
for (i = 0, len1 = caches.length; i < len1; i++) {
cache = caches[i];
if (cache[0] === currentTarget && cache[1] === 'unload') {
listeners = cache[2];
targetListener = cache[2];
console.log('listen unload');
if (typeof targetListener === 'function') {
targetListener.call(currentTarget, event);
} else if (isObject(targetListener) && typeof targetListener.handleEvent === 'function') {
targetListener.handleEvent(event);
}
} else {
cache[0].removeEventListener(cache[1], cache[2], cache[3]);
}
// console.log(cache);
}
this.setCacheAll(null);
currentTarget.removeEventListener(event.type, this, false);
// alert('unload completed');
};
this.addUnloadEventListener = function addUnloadEventListener () {
this.getGlobal().addEventListener('unload', this, false);
};
}).call(ListenerEvent.prototype);
return ListenerEvent;
})();
var JScriptEvent = (function () {
'use strict';
if (typeof attachEvent === 'undefined' || typeof detachEvent === 'undefined' || !isObject(attachEvent) || !isObject(detachEvent)) {
return;
}
/**
* included is-object.js
*
* @version 1.0.3
* @author think49
* @url https://gist.github.com/887049
*/
function isObject (value) {
return value !== null && typeof value !== 'undefined' && Object(value) === value;
}
function JScriptEvent (/*global*/) {
var caches, _global;
if (!(this instanceof JScriptEvent)) {
return new JScriptEvent(arguments[0]);
}
caches = [];
_global = isObject(arguments[0]) ? arguments[0] : Function('return this')();
this.getCacheAll = function getCacheAll () {
return caches;
};
this.setCacheAll = function setCacheAll (inputCaches) {
caches = inputCaches;
};
this.getGlobal = function getGlobal () {
return _global;
};
this.addUnloadEventListener();
}
(function () {
// Reference: DOM Level 3 Core -> Interface Node
var Node = (typeof Node === 'function' || typeof Node === 'object' && Node) ? Node : {
ELEMENT_NODE: 1,
ATTRIBUTE_NODE: 2,
TEXT_NODE: 3,
CDATA_SECTION_NODE: 4,
ENTITY_REFERENCE_NODE: 5,
ENTITY_NODE: 6,
PROCESSING_INSTRUCTION_NODE: 7,
COMMENT_NODE: 8,
DOCUMENT_NODE: 9,
DOCUMENT_TYPE_NODE: 10,
DOCUMENT_FRAGMENT_NODE: 11,
NOTATION_NODE: 12
};
function isWindow (node) {
return typeof node.document === 'object' && node.document.nodeType === Node.DOCUMENT_NODE;
}
function isDocument (node) {
return node.nodeType === Node.DOCUMENT_NODE;
}
function indexOfCache (caches, node, type) {
var cache, i;
i = caches.length;
while (i--) {
cache = caches[i];
if (cache[0] === node && cache[1] === type) {
return i;
}
}
return -1;
}
function getCache (caches, node, type) {
var i;
i = indexOfCache(caches, node, type);
return i !== -1 ? caches[i] : null;
}
function createNodeGetter (node) {
return function () {
return node;
};
}
function createListener (getNode, jscriptEvent) {
function listener (event) {
var cache, node, type, listeners, targetListener, i, len;
console.log('listen ' + event.type);
node = getNode();
type = event.type;
cache = getCache(jscriptEvent.getCacheAll(), node, type);
if (!cache) {
getNode = jscriptEvent = null;
return;
}
if (cache[3] !== listener) {
getNode = jscriptEvent = null;
return;
}
if (!('currentTarget' in event)) {
event.currentTarget = node;
}
if (!('target' in event)) {
event.target = event.srcElement;
if (!event.target) { // If event.target is a null
switch (type) {
case 'load':
case 'unload':
if (isWindow(node)) {
event.target = node.document;
}
break;
case 'DOMContentLoaded':
if (isWindow(node)) {
event.target = node.document;
} else if (isDocument(node)) {
event.target = node;
}
break;
default:
}
}
}
if (!('preventDefault' in event)) {
event.preventDefault = function () {
event.returnValue = type === 'mouseover'; // Reference: [HTML5] 7.1.6.1 Event handlers
};
}
if (!('stopPropagation' in event)) {
event.stopPropagation = function () {
event.cancelBubble = true;
};
}
listeners = cache[2];
for (i = 0, len = listeners.length; i < len; ++i) {
// console.log('listeners[' + i + '] = ' + listeners[i]);
targetListener = listeners[i];
if (typeof targetListener === 'function') {
targetListener.call(node, event);
} else if (isObject(targetListener) && typeof targetListener.handleEvent === 'function') {
targetListener.handleEvent(event);
}
}
}
return listener;
}
this.add = function add (node, type, listener) {
var caches, cache, listeners;
caches = this.getCacheAll();
cache = getCache(caches, node, type);
if (cache) {
cache[2].push(listener);
} else {
listeners = [listener];
listener = createListener(createNodeGetter(node), this);
node.attachEvent('on' + type, listener);
caches.push([node, type, listeners, listener]);
}
};
this.remove = function remove (node, type, listener) {
var caches, cache, listeners, i, j;
caches = this.getCacheAll();
i = indexOfCache(caches, node, type);
if (i !== -1) {
cache = caches[i];
listeners = cache[2];
j = listeners.length;
while (j--) {
if (listeners[j] === listener) {
delete listeners[j];
listeners[j] = listeners = listeners.slice(0, j).concat(listeners.slice(j + 1));
if (listeners.length < 1) {
node.detachEvent('on' + type, cache[3]);
delete caches[i];
this.setCacheAll(caches.slice(0, i).concat(caches.slice(i + 1)));
}
break;
}
}
}
};
this.addUnloadEventListener = function addUnloadEventListener () {
var that;
that = this;
function handleUnload (event) {
var type, currentTarget, caches, cache, listeners, listener, i, j, len;
type = event.type;
currentTarget = that.getGlobal();
caches = that.getCacheAll();
i = caches.length;
cache = getCache(caches, currentTarget, type);
if (cache) {
cache[3](event); // fire unload events
}
while (i--) {
cache = caches[i];
cache[0].detachEvent('on' + cache[1], cache[3]);
}
that.setCacheAll(null);
currentTarget.detachEvent('onunload', handleUnload);
that = handleUnload = null;
// alert('unload completed');
};
this.getGlobal().attachEvent('onunload', handleUnload);
this.getCacheAll().push([this.getGlobal(), 'unload', [], createListener(createNodeGetter(this.getGlobal()), that)]);
};
}).call(JScriptEvent.prototype);
return JScriptEvent;
})();
var CompatibleEvent = ListenerEvent || JScriptEvent;
@think49
Copy link
Author

think49 commented Mar 24, 2011

compatible-event.js

概要

このライブラリは大別して3つの機能があります。

  1. attachEvent の実行順を保証する (*備考1)
  2. attachEvent で DOM Events 相当のイベントプロパティが使えるようにする (ついでに this 値も event.currentTarget 相当にする)
  3. addEventListener, attachEvent で追加されたリスナー(イベントハンドラ)は window unload 時に削除される

1, 2 は IE (JScript) のための機能です。addEventListener と同等の機能を期待できます。

3 は循環参照に起因する問題で IE6SP2- に存在する 循環参照によるメモリリークパターン を解消します。
addEventListener でも実装しているのは「循環参照によるメモリリークは新規実装でも生じる可能性がある」と私が教わったためです。
実際に jquery.js や prototype.js も同様の実装をしているようです。
この実装によってイベントハンドラ関数(リスナー関数)でクロージャを形成し、クロージャがDOMノードを参照するコードを書いても、循環参照しなくなります。

3 が不要と判断できる場合は JScriptEvent 単体で使用することも出来ます。ListenerEvent, JScriptEvent には依存関係はありません。

(*備考1) IE の attachEvent で追加されたイベントハンドラはランダムに実行されることになっています。実際には一定の法則があるようですが、仕様ではランダムと規定されているので今後実装が変わる可能性があります。

既知の不具合

  • JScriptEvent で追加される listener において event.relatedTarget など一部のイベントプロパティを使用できません。
  • addEventListener の listener に { handleEvent: ... } を渡せるか、は DOM Events の規定外(実装依存)です。現在のバージョンでは window unload を追加したときには擬似的に { handleEvent: ... } を実装し、new ListenerEvent().add 時にはネイティブ実装を採用する挙動になっており、整合性がとれていません。

参考リンク

compatible-event.js

DOM Events

MSDN

HTML5

参考資料

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment