原味 JavaScript 的跨瀏覽器事件處理
Juriy Zaytsev | 2010 年 6 月 7 日
處理跨瀏覽器事件不是件容易的事。幸好多數盛行的 JavaScript 函式庫,已經透過抽象化,解決這個棘手過程。而這幕後的藏鏡人,是瀏覽器的不一致性,要鏟平這些差異,需要的是讓事件處理器能正常運作。讓我們來看看,如何才能建立強大的抽象化事件處理。不管你是想建立自己的事件處理工具,或是評估/優化既有工具,也或許純粹只是為了學習,這篇文章也許都起有所俾益。
事件處理的基礎,是從元素上加入或移除監聽事件的函式。而這個函式,正是我們要討論的對象。我們不會觸及發動事件的抽化象處理,這個議題過於深入,無法含蓋在本篇的範圍。
DOM 2 級定義 addEventListener and removeEventListener 是 EventTarget 介面 的一部分。
element.addEventListener(eventName, listener, useCapture);
element.removeEventListener(eventName, listener, useCapture);
實作 Node 或 Window 介面的物件,通常會實作 EventTarget 介面。這個意思是那個 DOM 元素,以及 window 物件,它們可以接受 addEventListener/removeEventListener 方法,而事件監聽器可以透過合作的物件加入或移除事件:
window.addEventListener('load', function (event) {
console.log('window\'s load event fired');
}, false);
// 或是
function bodyHandler() {
console.log('click event was fired on body element');
}
document.body.addEventListener('click', bodyHandler, false);
removeEventListener 同樣也相當直覺(注意,事件處理器應該使用相同的函式,也就是使用當初加到監聽器的那個函式—就像範例中的 bodyHandler):
document.body.removeEventListener('click', bodyHandler, false);
如果我們生活在一個完美的世界裡,要讓事件處理正常運作,需要的就是這兩個函式了。可惜事情沒有這麼美好,要達到這個目的,需要多花點工夫。橫在我們眼前的主要障礙,來自於 Internet Explorer 所使用的非標準事件處理模型。MSHTML DOM(用於 Internet Explorer 的文件物件模型)定義 attachEvent 和 detachEvent 方法,而不是採用 addEventListener/removeEventListener。這兩個方法,接受事件名稱字串作為第一個參數,而將事件處理函式物件作為第二個參數。
window.attachEvent('onload', function () {
console.log('window\'s load event fired');
});
function bodyHandler() {
console.log('click event was fired on body element');
}
document.body.attachEvent('onclick', bodyHandler);
document.body.detachEvent('onclick', bodyHandler);
如你所見,attachEvent 和 detachEvent 和標準有些微的不同。事件的名稱都會加上「on」這個前飾字,而第三個參數 useCapture 消失了。還有一些無法立即看到的不同之處,不過稍後我們會再解加介紹。
在有了這些知識之後,現在應該不難建立這兩個不同模型的抽象概念。讓我們來建立下面的包裝方法,addListener 和 removeListener:
function addListener(element, eventName, handler) {
if (element.addEventListener) {
element.addEventListener(eventName, handler, false);
}
else if (element.attachEvent) {
element.attachEvent('on' + eventName, handler);
}
else {
element['on' + eventName] = handler;
}
}
function removeListener(element, eventName, handler) {
if (element.addEventListener) {
element.removeEventListener(eventName, handler, false);
}
else if (element.detachEvent) {
element.detachEvent('on' + eventName, handler);
}
else {
element['on' + eventName] = null;
}
}
為了確保日後的相容性和互動性,優先測試標準方法(例如 addEventListener)是否存在相當重要,然後再去檢測特殊的方法(像是 attachEvent)。
要注意的是,除了 addEventListener 和 attachEvent 之外,還有第三個分支,就是元素同時缺少 addEventListener和attachEvent 方法。遇到這種情況,我們就退回非標準事件處理器的指派方法。
效能最佳化
注意每次 addListener/removeListener 如何檢測,這裡需要處理程式分支。方法透過元素被呼叫,而下一步的動作則取決於他們是否存在。我們可以透過在「載入期間」定義不同的函式(而非「執行期間」),消除所有非必要的工作。舉例來說,在宣告 addListener 的時候,我們可以檢查 addEventListener 或 attachEvent 在同一 DOM 元素中是否存在,這時會存取一次 DOM。這個元素是文件中的根元素,他是像 HTML 的 <html>...</html> 標籤,並且可以透過 DOM 的 document.documentElement 來存取。相對於 body 元素(可由 DOM 中的 document.body 存取 ),document.documentElement 在 document 還沒「準備就緒」前,就已經存在。這顯然讓它成為頁面尚未完成下載前,進行檢測工作的完美對象。而另一個「測試元素」的侯選人,是用動態方式建立任何元素,例如 document.createElement('div')。
讓我們來看載入期間分析是如何完成的:
/* 首先,宣告變數指派給函式使用*/
var addListener, removeListener,
/* 測試元素 */
docEl = document.documentElement;
if (docEl.addEventListener) {
/* 假如「addEventListener」存在於測試元素,定義函式使用「addEventListener」*/
addListener = function (element, eventName, handler) {
element.addEventListener(eventName, handler, false);
};
}
else if (docEl.attachEvent) {
/* 假如「attachEvent」存在於測試元素,定義函式使用「attachEvent」 */
addListener = function (element, eventName, handler) {
element.attachEvent('on' + eventName, handler);
};
}
else {
/* 假如兩個函式在測試元素上均不存在,定義函式退回策略 */
addListener = function (element, eventName, handler) {
element['on' + eventName] = handler;
};
}
而這對 removeListener 也同樣適用;函式在載入期間被定義,依據詢問函式存在於否的結果。
if (docEl.removeEventListener) {
removeListener = function (element, eventName, handler) {
element.removeEventListener(eventName, handler, false);
};
}
else if (docEl.detachEvent) {
removeListener = function (element, eventName, handler) {
element.detachEvent('on' + eventName, handler);
};
}
else {
removeListener = function (element, eventName, handler) {
element['on' + eventName] = null;
};
}
值得一提的是,這個最佳化有它的缺點。除了增加程式大小,它用了一個稍微脆弱、迂迴的推論。意思是它放棄直接在目標元素上檢測方法是否存在,而改在測試測試元素測試,並且假設這個方法也一樣存在於目標元素上。只有要假設,就有失敗的風險。理論上 document.documentElement 可以實作 addEventListener,但是另一個元素,傳給 addListener 那個,就不會有這個方法。就實作的經驗顯示,當它是 addEventListener/removeEventListener 的時候,這樣的假設通常是安全的,只要我們在相同物件類型上測試這個方法,例如都在元素物件上測試,而不是 window 或 document 物件。
強化假設
說到迂迴的假設,我們可以做一件小事來降低失敗的機率。我們也可以測試 window 物件是否存在方法。畢竟 addListener 通常會被傳給 window 物件(監聽像是「load」、「resize」、「scroll」等事件):
var addListener, docEl = document.documentElement;
if (docEl.addEventListener && window.addEventListener) {
addListener = function (element, eventName, handler) {
/* ... */
};
}
else if (docEl.attachEvent && window.attachEvent) {
addListener = function (element, eventName, handler) {
/* ... */
};
}
else {
/* ... */
}
在 window 物件上缺少 addEventListener 或 attachEvent 實作,將不會再危害 addListener 了。從 document.documentElement 上是否存在這個方法,我們假設所有實作 Node 介面的物件上,都會有這個方法。(像是 DOM 元素實作了 Element 介面,或是文件實作了 Document 介面)。而從 window 存在這個方法,我們假設所有的 window 物件都擁有這個方法。
安全故障(Fail-safe)功能測試
我們測試 DOM 元素(還有其他物件,像是 window)的方式,是藉由執行布林值的型別轉換。布林值型別轉換工作會發生,是在if宣告中的值,被當成是表達式。(例如:if (docEl.addEventListener) { ... })。這樣的轉換存在著問題,它會在某些實作中失敗:
// 在 Internet Explorer 中
var xhr = new ActiveXObject('Microsoft.XMLHTTP');
if (xhr.open) { } // 錯誤
var element = document.createElement('p');
if (element.offsetParent) { } // 錯誤
在 Internet Explorer,這樣的物件被實作為 ActiveX 物件,而 他們的型別轉換結果有誤。這裡有趣的地方在於,這些物件的型別(當由 typeof 運算子返回時)通常是「unknown」。假如你覺得這是一個奇怪的型別,那是因為它的確是。不過按照規格書(ECMA-262,第三版),typeof 運算子可以返回任何已知物件的值(像是「foo」、「bar」、「number」、「undefined」或甚至於空字串)。
所以我們該如何處理這些詭異的物件?既然我們不知道他們的真正型別,最安全的策略是在任何物件上避用型別轉換。改絃易轍的作法,我們可以透過偵測型別(當 typeof 運算子返回時),推斷方法存在與否或它的能力。假如型別是「object」或「function」,我們假定這個物件可以呼叫。假如型別是「unknown」,我們也可假定這個物件同樣可以呼叫,只是這個物件是一個 ActiveX 物件,但仍可以呼叫。
為了抽象化這個型別檢查,一些函式使用所謂的 isHostMethod(由 David Mark 而知名,現在使用在 FuseJS 和 My Library 中):
var isHostMethod = function (object, methodName) {
var t = typeof object[methodName];
return ((t === 'function' || t === 'object') && !!object[methodName]) || t === 'unknown';
};
我們現在可以取代所有物件的型別檢查,讓測試更為安全,像是 isHostMethod 為基礎的檢查:
var addListener, docEl = document.documentElement;
if (isHostMethod(docEl, 'addEventListener')) {
/* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
/* ... */
}
else {
/* ... */
}
document.documentElement 實作的 addEventListener 或 attachEvent 的方法詭異,現在處理時已經不會再有錯誤了。
正規化 IE 中的事件處理器
我之前提過 MSHTML 事件模型與標準事件模型在幾個地方有所出入。這些差異之中,有一個是發生在事件處理器被調用時的處理方式。處理器藉由 addEventListener 初始化,總是被監聽器被附加的元素的情境(context)中被呼叫。在 document.body.addEventListener(...) 的例子中,事件處理器被呼叫,是在 document.body 的情境中。而像在 window.addEventListener('...') 例子中,事件處理器是在 window 物件的情境中被呼叫。
然而 attachEvent 的行為與其他不同,它的事件處理器被呼叫時,總是在 window 物件的情境中:
document.body.attachEvent('onclick', function () {
console.log(this === window); // 真
console.log(this === document.body); // 假
});
因此,我們的 addListener 如同它現在所代表的,和事件處理器的情境並不一致。假如你計劃使用它,消除這個危險的不一致性是個好主意。讓我們看看如何辦到:
if (isHostMethod(docEl, 'addEventListener')) {
/* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
addListener = function (element, eventName, handler) {
element.attachEvent('on' + eventName, function () {
handler.call(element, window.event);
});
};
}
else {
/* ... */
}
不將事件處理器直接傳給 attachEvent,我們改採傳遞一個包裝函式,它會改變呼叫事件處理器的情境。現在會一直參照監聽器被附加的元素。
addListener(document.body, function () {
console.log(this === window); // 假
console.log(this === document.body); // 真
});
你也許已經注意到,不止事件處理器在一個元素的情境中被呼叫,而且它也被傳遞給 window.event 物件作為第一個參數— handler.call(element, window.event)。
這樣做是為了解決另一個 MSHTML 事件模式的問題,缺少事件物件作為事件處理器的第一個函式。當 addEventListener 確保事件處理器被指定為給一個事件物件,Internet Explorer 讓事件物件可以被存取,藉由全域的 window.event 屬性。沒有任何東西被傳遞給事件處理器。
透過明確傳遞 window.event 給事件處理器,藉由 handler.call(element, window.event),我們確保它總是會有適當、可存取的物件模型作為第一個參數。
addListener(document.body, function (event) {
console.log(typeof event != 'undefined'); // 真
});
清除 IE 的記憶體洩露
雖然前面的方面解決了問題,卻也帶來了另一個問題,那就是偷偷摸摸的記憶體洩露。Internet Explorer 惱人的記憶體洩露問題已經在這被詳細描述過還有這裡,所以再此我們就不再多說。然而,讓我們很快地來看一下究竟是什麼樣特別的情況會造成這個問題:
...
addListener = function (element, eventName, handler) {
element.attachEvent('on' + eventName, function () {
handler.call(element, window.event);
});
};
...
當 addListener 函式執行時,一個循環參照發生了。一個元素參照到事件處理器,而事件處理器又透過範圍鏈參照到元素。接下來的模式就是一個典型的記憶體洩露,這也是我們需要特別處理的地方。
所以如何修復這個洩露?一個最簡單的作法,就是在頁面卸載時,中斷這個反覆循環參照。這個想法很簡單:在 window 卸載時觸發事件,遍歷所有存在的事件監聽器,然後清除掉。所謂清除動作,我們指的是移除事件,取消它的參照。這樣可以有效地中斷循環參照。
讓我們來檢視這樣的系統可能的實作方法:
function wrapHandler(element, handler) {
return function (e) {
return handler.call(element, e || window.event);
};
}
function createListener(element, eventName, handler) {
return {
element: element,
eventName: eventName,
handler: wrapHandler(element, handler)
};
}
function cleanupListeners() {
for (var i = listenersToCleanup.length; i--; ) {
var listener = listenersToCleanup[i];
litener.element.detachEvent(listener.eventName, listener.handler);
listenersToCleanup[i] = null;
}
window.detachEvent('onunload', cleanupListeners);
}
var listenersToCleanup = [ ];
if (isHostMethod(docEl, 'addEventListener')) {
/* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
addListener = function (element, eventName, handler) {
var listener = createListener(element, eventName, handler);
element.attachEvent('on' + eventName, listener.handler);
listenersToCleanup.push(listener);
};
window.attachEvent('onunload', cleanupListeners);
}
else {
/* ... */
}
在 addListener 宣告的時候,我們也附加了「unload」的監聽器到 window 上。這個方法結束所有儲存在 listenersToCleanup 陣列的監聽器,然後針對它們呼叫 detachEvent 方法,接著清空參照。
在 addListener 執行時,我們建立一個抽象的監聽器,一個封裝元素的物件,eventName,然後正規化處理器,之後再將監聽器放入 listenersToCleanup 陣列,以便進行稍後的清除工作。
避免在 IE 中造成記憶體洩露
上一節我們所寫的清除程序在和記憶體洩露保持距離上做得不錯。但是,另一個問題卻偷偷摸摸的抬頭-使用卸載監聽器,造成頁面暫存失效,一個在現代瀏覽器都有實作的驚人功能。他以 bfcache 之名廣為人知,頁面暫存是一個提供頁面立即顯示功能,能讓使用者點選上一頁/下一頁時使用,而不用再跟伺服器要資料。不幸的是,一旦「unload」監聽器被加到 window,頁面暫存會被停用。頁面暫存被停用的原因,是避免在卸載時的事件處理器中,文件可能有所修改。這時使用暫存的版本可能會有風險。因此,文章必須重新和伺服器要資料。
由於禁用暫存的緣故,我們不能使用 unload 監聽器,但我們也不想讓記憶體洩露的循環參照就擺在那裡。該如何處理這個棘手的問題?
其實解決方案異外簡單;一開始就別造成循環參照!
我們需要清除的原因在於難以找出循環參照。現在讓我們試試找出那段偷偷摸摸的程式:
addListener = function (element, eventName, handler) {
...
element.attachEvent('on' + eventName, listener.handler);
...
};
function wrapHandler(element, handler) {
return function (e) {
return handler.call(element, e || window.event);
};
}
function createListener(element, eventName, handler) {
return {
...
handler: wrapHandler(element, handler)
};
}
注意 addListener 如何藉由 attachEvent 附加 listener.handler 到元素上去。現在反過來,listener.handler 是由 wrapHandler(element, handler) 所建立。假如我們檢視一下 wrapHandler,就會變得清晰,它返回函式(變成事件處理器那個)關閉了問題元素。再一次強調,一個元素參考一個事件處理器,而事件處理器參考到一個元素。那就生成了一個循環參照。
所以怎麼樣才可以避免產生這樣的循環參照?讓我們來看看一個可能的實作。
下面程式的目標是避免在事件處理器中去參考一個元素。這是消除巢狀循環參照的關鍵。一反傳遞元素到處理器的建造者,我們傳一個獨特的 id,讓我們之後可用來存取元素。那麼讓如何讓一個元素一開始就有id呢?這可以在 addListener 一開始的時候,透過指定唯一的 id 來達成。
因此概括來說,主要實作 addListener 的步驟如下:
- 取得元素的唯一 id
- 建立一個監聽器,它有一個包裝處理器(已經正規化過),這個包裝處理器永遠不會接受一個實際的元素,以避免循環參照。
- 新建立的監聽器和唯一的 id 和事件型別作關聯
- 包裝處理器(正規化過)被附加到一個元素(藉由 attachEvent)。
循環參照成功地避開。
var getUniqueId = (function () {
if (typeof document.documentElement.uniqueID != 'undefined') {
return function (element) {
return element.uniqueID;
};
}
var uid = 0;
return function (element) {
return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++);
};
})();
var getElement, setElement, listeners = { };
(function () {
var elements = { };
getElement = function (uid) {
return elements[uid];
};
setElement = function (uid, element) {
elements[uid] = element;
};
})();
function createListener(uid, handler) {
return {
handler: handler,
wrappedHandler: createWrappedHandler(uid, handler)
};
}
function createWrappedHandler(uid, handler) {
return function (e) {
handler.call(getElement(uid), e || window.event);
};
}
var addListener = function (element, eventName, handler) {
var uid = getUniqueId(element);
setElement(uid, element);
if (!listeners[uid]) {
listeners[uid] = { };
}
if (!listeners[uid][eventName]) {
listeners[uid][eventName] = [ ];
}
var listener = createListener(uid, handler);
listeners[uid][eventName].push(listener);
element.attachEvent('on' + eventName, listener.wrappedHandler);
};
var removeListener = function (element, eventName, handler) {
var uid = getUniqueId(element), listener;
if (listeners[uid] && listeners[uid][eventName]) {
for (var i = 0, len = listeners[uid][eventName].length; i < len; i++) {
listener = listeners[uid][eventName][i];
if (listener && listener.handler === handler) {
element.detachEvent('on' + eventName, listener.wrappedHandler);
listeners[uid][eventName][i] = null;
}
}
}
};
在撰寫這篇文章的時候,只有少數幾個 JavaScript 函式庫,應用了事件處理器抽象化,無需指派 unload 監聽器。FuseJS 是其中之一。其他處理這個問題的辦法,是只在 IE 指派 unload 監聽器,判斷是否為 IE,是由偵測使用者代理程式字串或基於同類型的推斷測試(以 jQuery 1.4.2 來說,只在 window.attachEvent 存在時才指派 unload,如果是 window.addEventListener 就不指派)。和脆弱、難以預測的使用者代理程式字串比較,物件推斷(例如 jQuery 使用的方法)是用較安全的方法來代替。不過如同其他許多推論,它有產生誤報的風險。在一開始就避免附加 unload 監聽器,是避免任何潛在失敗的實作。
修正 DOM 0 級的分支
我們已經做過正規化、處理 IE 事件分支,並且避免記憶體洩露發生。而且我們不止避免了記憶體洩露發生,還保留頁面的暫存機制。我們使用穩健的測試和推論。我們也在載入時期,優化分支方法的效能。我們還有忘了什麼?不幸的是,我們實作的第三個分支,那個處理既非 addEventListener,也不是 attachEvent 的時候的備用方案,目前已經消失了。
如果你仔細查看,問題應該很明顯。
var addListener;
if (isHostMethod(docEl, 'addEventListener')) {
/* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
/* ... */
}
else {
addListener = function (element, eventName, handler) {
element['on' + eventName] = handler;
};
}
每次 addListener 被執行時,一個新的處理器會覆寫的既有的!此外,目前的 removeListener,會完全移除元素上的所有事件處理器。這當然和其他實作不相符合,因此要不就是移掉第三個分支,不然就是修好它。
讓我們先來看一下,如果要移除第三個分支,應該怎麼做。一些較老舊的瀏覽器(像是 Netscape Navigator 4、Opera 6)同時缺少 addEventListener 和 attachEvent,所以第三個分支是他們唯一可用的方案。然而,這些瀏覽器現在可能已經絕跡,更需要關注的可能是行動裝置的瀏覽器,這些瀏覽器缺少基本 DOM 的支援已經是眾所皆知。假如我們不希望第三個分支運作,我們至少應該避免錯誤發生,並通知使用者他們在這個實作上有所不足。
var addListener;
...
else {
addListener = function () {
/* noop */
};
addListener.defunct = true;
}
注意我們仍然定義了 addListene 函式,但不讓它運作。我們標註它作為「停用」藉由指定「defunct」屬性為真。用戶端程式現在可以發現 addListener 是否在運作。如果功能停用,應用程式可以優雅地降級使用。
if (!addListener.defunct) {
/*
Tab面板只在「addListener」正常運作時才會初始化,
因此「click」事件能正常地處理
此外,原來不具程式功能的版本會被顯示
(例如章節的列表)
這個可以防止我們初化化tab面板
以免click事件無法處理,而造成壞掉的感覺
*/
tabPanel.initialize();
addListener(tabPanel, 'click', handleTabClick);
}
從另一方面來說,如果目標是支援沒有 addEventListener / attachEvent 的瀏覽器,以 DOM 0 級作為實作的基準,保持這樣的一致性就是個好主意。
在我們開始檢視實作前,下面是事件流程的摘要。當 addListener 被調用時,會去找一個包含唯一 id 的元素。我們也會在 attachEvent 分支中使用相同的協助函式。下一步,這個 id 關聯到一個物件,這個物件會包含所有這個元素的處理器。再下一步,一個事件處理器被存入佇列中,這個佇列存放所有特定元素和型別的事件處理器。最後,既存的事件處理器,被一個發送器(dispatcher)取代,它的工作是遍歷所有特定元素/型別組合的事件處理器,然後在特定的情境(元素)和適當的參數(事件)時執行事件處理器。
...
else {
var createDispatcher = function (uid, eventName) {
return function (e) {
if (handlers[uid] && handlers[uid][eventName]) {
var handlersForEvent = handlers[uid][eventName];
for (var i = 0, len = handlersForEvent.length; i < len; i++) {
handlersForEvent[i].call(this, e || window.event);
}
}
};
};
var handlers = { };
addListener = function (element, eventName, handler) {
var uid = getUniqueId(element);
if (!handlers[uid]) {
handlers[uid] = { };
}
if (!handlers[uid][eventName]) {
handlers[uid][eventName] = [ ];
var existingHandler = element['on' + eventName];
if (existingHandler) {
handlers[uid][eventName].push(existingHandler);
}
element['on' + eventName] = createDispatcher(uid, eventName);
}
handlers[uid][eventName].push(handler);
};
removeListener = function (element, eventName, handler) {
var uid = getUniqueId(element);
if (handlers[uid] && handlers[uid][eventName]) {
var handlersForEvent = handlers[uid][eventName];
for (var i = 0, len = handlersForEvent.length; i < len; i++) {
if (handlersForEvent[i] === handler) {
handlersForEvent.splice(i, 1);
}
}
}
};
}
現在 DOM 0 級分支已經被和其他實作有一致性。
還有什麼沒被處理
即使最後實作的結果強大而完整,不過還是有幾件事我們沒有處理到。其中之一是在 Internet Explorer 中支援補捉階段(capturing phase)。MSHTML 事件模型中缺少補捉階段的支援,就是 addListener 不允許我們指定 useCapture 參數的原因(即使標準的 addEventListener 可以處理)。
另一個不一致性是 Internet Explorer 中的事件處理器執行順序。在 DOM 2 級中,事件模組沒有指定事件處理器的觸發順序,多數的瀏覽器依循 FIFO(先進先出),而非 MSHTML 事件模型中的 LIFO(後進先出)。DOM 3 級的事件模型(目前是草稿)則 明確指定 FIFO 的順序,也就是「所有已經註冊到目標的事件處理器依他們的註冊順序」執行。
我們也還沒有處理 DOM 0 級分支的發送器在獨立事件處理器的錯誤。一般來說,確保沒有事件處理器被其他事件處理器所影響是個好主意。
當我們取得一個元素的特定 ID,我們沒有處理萬一這個元素不是元素,而是像 window 物件時該怎麼辦。
我們可以在下一次解決這些「問題」。就目前而言,它們就當作是給讀者的練習。最後,下面是約 150 行左右,完整 addListener / removeListener 實作的程式。
最終實作內容
(function(global){
function areHostMethods(object) {
var methodNames = Array.prototype.slice.call(arguments, 1),
t, i, len = methodNames.length;
for (i = 0; i < len; i++) {
t = typeof object[methodNames[i]];
if (!(/^(?:function|object|unknown)$/).test(t)) return false;
}
return true;
}
var getUniqueId = (function () {
if (typeof document.documentElement.uniqueID !== 'undefined') {
return function (element) {
return element.uniqueID;
};
}
var uid = 0;
return function (element) {
return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++);
};
})();
var getElement, setElement;
(function () {
var elements = { };
getElement = function (uid) {
return elements[uid];
};
setElement = function (uid, element) {
elements[uid] = element;
};
})();
function createListener(uid, handler) {
return {
handler: handler,
wrappedHandler: createWrappedHandler(uid, handler)
};
}
function createWrappedHandler(uid, handler) {
return function (e) {
handler.call(getElement(uid), e || window.event);
};
}
function createDispatcher(uid, eventName) {
return function (e) {
if (handlers[uid] && handlers[uid][eventName]) {
var handlersForEvent = handlers[uid][eventName];
for (var i = 0, len = handlersForEvent.length; i < len; i++) {
handlersForEvent[i].call(this, e || window.event);
}
}
};
}
var addListener, removeListener,
shouldUseAddListenerRemoveListener = (
areHostMethods(document.documentElement, 'addEventListener', 'removeEventListener') &&``
areHostMethods(window, 'addEventListener', 'removeEventListener')),
shouldUseAttachEventDetachEvent = (
areHostMethods(document.documentElement, 'attachEvent', 'detachEvent') &&
areHostMethods(window, 'attachEvent', 'detachEvent')),
// IE 分支
listeners = { },
// 0 級 DOM 分支
handlers = { };
if (shouldUseAddListenerRemoveListener) {
addListener = function (element, eventName, handler) {
element.addEventListener(eventName, handler, false);
};
removeListener = function (element, eventName, handler) {
element.removeEventListener(eventName, handler, false);
};
}
else if (shouldUseAttachEventDetachEvent) {
addListener = function (element, eventName, handler) {
var uid = getUniqueId(element);
setElement(uid, element);
if (!listeners[uid]) {
listeners[uid] = { };
}
if (!listeners[uid][eventName]) {
listeners[uid][eventName] = [ ];
}
var listener = createListener(uid, handler);
listeners[uid][eventName].push(listener);
element.attachEvent('on' + eventName, listener.wrappedHandler);
};
removeListener = function (element, eventName, handler) {
var uid = getUniqueId(element), listener;
if (listeners[uid] && listeners[uid][eventName]) {
for (var i = 0, len = listeners[uid][eventName].length; i < len; i++) {
listener = listeners[uid][eventName][i];
if (listener && listener.handler === handler) {
element.detachEvent('on' + eventName, listener.wrappedHandler);
listeners[uid][eventName][i] = null;
}
}
}
};
}
else {
addListener = function (element, eventName, handler) {
var uid = getUniqueId(element);
if (!handlers[uid]) {
handlers[uid] = { };
}
if (!handlers[uid][eventName]) {
handlers[uid][eventName] = [ ];
var existingHandler = element['on' + eventName];
if (existingHandler) {
handlers[uid][eventName].push(existingHandler);
}
element['on' + eventName] = createDispatcher(uid, eventName);
}
handlers[uid][eventName].push(handler);
};
removeListener = function (element, eventName, handler) {
var uid = getUniqueId(element);
if (handlers[uid] && handlers[uid][eventName]) {
var handlersForEvent = handlers[uid][eventName];
for (var i = 0, len = handlersForEvent.length; i < len; i++) {
if (handlersForEvent[i] === handler) {
handlersForEvent.splice(i, 1);
}
}
}
};
}
/* 匯出作為全域屬性*/
global.addListener = addListener;
global.removeListener = removeListener;
})(this);