使用類庫可以比較容易的解決相容性問題.但這背後的機理又是如何呢? 下面我們就一點點鋪開來講.
首先,DOM Level2為事件處理定義了兩個函數addEventListener和removeEventListener, 這兩個函數都來自於EventTarget介面.
複製代碼 代碼如下:element.addEventListener(eventName, listener, useCapture);
element.removeEventListener(eventName, listener, useCapture);
EventTarget介面通常實現自Node或Window介面.也就是所謂的DOM元素.
那麼比如window也就可以通過addEventListener來添加監聽. 複製代碼 代碼如下:function loadHandler() {
console.log('the page is loaded!');
}
window.addEventListener('load', loadHandler, false);
移除監聽通過removeEventListener同樣很容易做到, 只要注意移除的控制代碼和添加的控制代碼引用自一個函數就可以了.
window.removeEventListener('load', loadHandler, false);
如果我們活在完美世界.那麼估計事件函數就此結束了.
但情況並非如此.由於IE獨樹一幟.通過MSDHTML DOM定義了attachEvent和detachEvent兩個函數取代了addEventListener和removeEventListener.
恰恰函數間又存在著很多的差異性,使整個事件機制變得異常複雜.
所以我們要做的事情其實就轉移成了.處理IE瀏覽器和w3c標準之間對於事件處理的差異性.
在IE下添加監聽和移除監聽可以這樣寫 複製代碼 代碼如下:function loadHandler() {
alert('the page is loaded!');
}
window.attachEvent('onload', loadHandler); // 添加監聽
window.detachEvent('onload', loadHandler); // 移除監聽
從表象看來,我們可以看出IE與w3c的兩處差異:
1. 事件前面多了個"on"首碼.
2. 去除了useCapture第三個參數.
其實真正的差異遠遠不止這些.等我們後面會繼續分析.那麼對於現在這兩處差異我們很容易就可以抽象出一個公用的函數 複製代碼 代碼如下: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;
}
}
上面函數有兩處需要注意一下就是:
1. 第一個分支最好先測定w3c標準. 因為IE也漸漸向標準靠近. 第二個分支監測IE.
2. 第三個分支是留給既不支援(add/remove)EventListener也不支援(attach/detach)Event的瀏覽器.
效能最佳化
對於上面的函數我們是運用"運行時"監測的.也就是每次綁定事件都需要進行分支監測.我們可以將其改為"運行前"就確定相容函數.而不需要每次監測.
這樣我們就需要用一個DOM元素提前進行探測. 這裡我們選用了document.documentElement. 為什麼不用document.body呢? 因為document.documentElement在document沒有ready的時候就已經存在. 而document.body沒ready前是不存在的.
這樣函數就最佳化成 複製代碼 代碼如下:var addListener, removeListener,
/* test element */
docEl = document.documentElement;
// addListener
if (docEl.addEventListener) {
/* if `addEventListener` exists on test element, define function to use `addEventListener` */
addListener = function (element, eventName, handler) {
element.addEventListener(eventName, handler, false);
};
}
else if (docEl.attachEvent) {
/* if `attachEvent` exists on test element, define function to use `attachEvent` */
addListener = function (element, eventName, handler) {
element.attachEvent('on' + eventName, handler);
};
}
else {
/* if neither methods exists on test element, define function to fallback strategy */
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具備了add/remove方法.那麼element就一定具備(雖然大多數情況如此).但這顯然是不夠安全.
不安全的檢測
下面兩個例子說明.在某些情況下這種檢測不是足夠安全的. 複製代碼 代碼如下:// In Internet Explorer
var xhr = new ActiveXObject('Microsoft.XMLHTTP');
if (xhr.open) { } // Error
var element = document.createElement('p');
if (element.offsetParent) { } // Error
如: 在IE7下 typeof xhr.open === 'unknown'. 詳細可參考feature-detection
所以我們提倡的檢測方式是 複製代碼 代碼如下:var isHostMethod = function (object, methodName) {
var t = typeof object[methodName];
return ((t === 'function' || t === 'object') && !!object[methodName]) || t === 'unknown';
};
這樣我們上面的最佳化函數.再次改進成這樣 複製代碼 代碼如下:var addListener, docEl = document.documentElement;
if (isHostMethod(docEl, 'addEventListener')) {
/* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
/* ... */
}
else {
/* ... */
}
丟失的this指標
this指標的處理.IE與w3c又出現了差異.在w3c下函數的指標是指向綁定該控制代碼的DOM元素. 而IE下卻總是指向window. 複製代碼 代碼如下:// IE
document.body.attachEvent('onclick', function () {
alert(this === window); // true
alert(this === document.body); // false
});
// W3C
document.body.addEventListener('onclick', function () {
alert(this === window); // false
alert(this === document.body); // true
});
這個問題修正起來也不算麻煩 複製代碼 代碼如下: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 {
/* ... */
}
我們只需要用一個封裝函數.然後在內部將handler用call重新修正指標.其實大夥應該也看出了,這裡還偷偷的修正了一個問題就是.IE下event不是通過第一個函數傳遞,而是遺留在全域.所以我們經常會寫event = event || window.event這樣的代碼. 這裡也一併做了修正.
修正了這幾個主要的問題.我們這個函數看起來似乎健壯了很多.我們可以暫停一下做下簡單的測試, 測試三點
1. 各瀏覽器安全色 2. this指標指向相容 3. event參數傳遞相容.
測試代碼如下: xmlns="http://www.w3.org/1999/xhtml">
測試文本