Last active
September 25, 2015 07:07
-
-
Save think49/882821 to your computer and use it in GitHub Desktop.
compatible-event.js : addEventListener, attachEvent のラッパー関数。addEvent したリスナーは window unload 時に削除される(循環参照対策)。attachEvent でも実行順が保証される。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
compatible-event.js
概要
このライブラリは大別して3つの機能があります。
1, 2 は IE (JScript) のための機能です。addEventListener と同等の機能を期待できます。
3 は循環参照に起因する問題で IE6SP2- に存在する 循環参照によるメモリリークパターン を解消します。
addEventListener でも実装しているのは「循環参照によるメモリリークは新規実装でも生じる可能性がある」と私が教わったためです。
実際に jquery.js や prototype.js も同様の実装をしているようです。
この実装によってイベントハンドラ関数(リスナー関数)でクロージャを形成し、クロージャがDOMノードを参照するコードを書いても、循環参照しなくなります。
3 が不要と判断できる場合は JScriptEvent 単体で使用することも出来ます。ListenerEvent, JScriptEvent には依存関係はありません。
(*備考1) IE の attachEvent で追加されたイベントハンドラはランダムに実行されることになっています。実際には一定の法則があるようですが、仕様ではランダムと規定されているので今後実装が変わる可能性があります。
既知の不具合
参考リンク
compatible-event.js
DOM Events
MSDN
HTML5
参考資料