DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> jQuery入門知識 >> JQuery特效代碼 >> 細說浏覽器特性檢測(2)-通用事件檢測
細說浏覽器特性檢測(2)-通用事件檢測
編輯:JQuery特效代碼     

事件檢測,即檢測某一事件在不同的浏覽器中是否存在(可用),這在編寫Javascript的過程中也非常重要,如mouseenter/mouseleave事件雖然實用,但並不是所有浏覽器都提供了標准的支持,因此需要自己手動模擬,即:

function addEvent(element, name, handler) { 
if (name == 'mouseenter' && !hasEvent(name, element)) { 
//通過其他手段模擬mouseenter事件 
} 
//正常的事件注冊 
};

本文就重點講述以上代碼中hasEvent的具體實現。

基本方案

關於事件的最基本檢測方式,則需要從事件的注冊方法開始說。

事件通常有3種注冊方式,其中之一就是內聯式,即在HTML中通過屬性的方式聲明事件,比如:

<button onclick="alert('CLICKED!');">CLICK ME</button>

以上代碼創建了一個button標簽,並注冊了click事件。

另一個方案是通過直接給onclick賦值來注冊事件:

document.getElementById('myButton').onclick = function() { 
alert('CLICKED!'); 
};

從上面兩種注冊事件的方式可以發現,其實onclick是button標簽的一種屬性(attribute),通過對其賦值可以完成事件的注冊。

因此,最基本的事件檢測方案,就是通過檢查on[事件名]屬性是否存在於DOM元素之中,因此有最簡單的一個版本:

function hasEvent(name, element) { 
name = name.indexOf('on') ? 'on' + name : name; 
element = element || document.createElement('div'); 
var supported = name in element; 
};

需要注意的是,事件是對on[事件名]的形式作為元素的屬性而存在的,因此從通用性上考慮,在必要的時候對事件名補上'on'即可。另外由於是一個通用的判斷事件是否可用的函數,當沒有給定具體的元素時,可以使用最廣泛應用的div元素作為替代。

部分標簽特有事件

有些事件是一些元素特有的,通常包括以下幾個:

  • form獨有事件:submit、reset
  • input獨有事件:change、select
  • img獨有事件:load、error、abort

考慮到這些事件的存在,使用div元素有時會得到錯誤的結果,因此在創建一個通用的替代用元素時,可以使用一個字典來維護需要創建的元素標簽名:

var hasEvent = (function() { 
var tags = { 
onsubmit: 'form', onreset: 'form', 
onselect: 'input', onchange: 'input', 
onerror: 'img', onload: 'img', onabort: 'img' 
}; 
return function(name, element) { 
name = name.indexOf('on') ? 'on' + name : name; 
element = element || document.createElement(tags[name] || 'div'); 
supported = name in element; 
} 
})();

使用閉包將tags作為靜態的字典使用,可以在一定程度上減少對象生成的開銷。

DOM污染

DOM元素之所以會有類似onclick的屬性,是因為在DOM元素對象的__proto__中有這個屬性,由於Javascript弱類型機制,外部代碼可以通過對__proto__添加屬性而影響hasEvent函數的結果,如以下代碼在Firefox和Chrome中就會產生錯誤的結果:

document.createElement('div').__proto__.ontest = function() {}; 
var supported = hasEvent('test', document.createElement('div')); //true

在上面的示例中,雖然在修改__proto__屬性和調用hasEvent時,使用的是不同的div對象,但由於__proto__的實質是原型鏈中的對象,因此會影響到所有的div對象。

為了處理這種情況,需要嘗試將__proto__屬性中相應的屬性進行刪除,由於原生類型的屬性帶有DontDelete標記,是無法使用delete關鍵字進行刪除的,因此對hasEvent函數附加以下的邏輯就可以更安全地判斷:

var temp; 
if (supported && (temp = proto[name]) && delete proto[name]) { 
supported = name in element; 
proto[name] = temp; 
}

邏輯很簡單,嘗試把__proto__中有可能附加上去的刪了再試一試,當然別忘了再把原來的值變回去。

Firefox開始BUG

很遺憾,前文提供的hasEvent函數並不能在Firefox完美工作,在Firefox中運行以下代碼將得到false的結果:

alert('onclick' in document.documentElement); //Firefox彈出false

因此,需要再次改造hasEvent函數以支持Firefox。在多數浏覽器中,當元素使用內聯方式注冊了事件之後,可以通過element.on[事件名]來獲取注冊在上面的函數對象,例如:

<button id="test" onclick="alert('CLICKED!');" ontest="alert('TEST!');">CLICK ME</button> 
<script type="text/javascript"> 
var button = document.getElementById('test'); 
alert(typeof button.onclick); //彈出function 
alert(typoef button.ontest); //彈出string 
</script>

因此,只需要通過Javascript將一個表示函數的字符串掛載到on[事件名]屬性(attribute)上,再去獲取並判斷是否得到了一個函數對象即可。

因此hasEvent函數在前文提供的方法返回false時,可以額外增加以下的代碼以進一步確定事件是否存在:

if (!supported) { 
element.setAttribute(name, 'return;'); 
supported = typeof element[name] == 'function'; 
}

Firefox繼續BUG

到現在為止,已經可以在兼容多數浏覽器的情況下檢測各DOM元素的事件,但是對於window對象的事件檢測還沒有一個完整的方案。

對於IE系列、Chrome和Safari,都可以使用簡單的on[事件名] in window檢測事件是否存在,因此原有的提供防止DOM污染後的hasEvent函數可以很好地完成任務。

唯有Firefox上,以下代碼會給出錯誤的結果:

alert('onload' in window); //Firefox彈出false 
alert('onunload' in window); //Firefox彈出false 
alert('onerror' in window); //Firefox彈出false

值得慶幸也值得憤怒的是,Firefox很詭異地可以在div等元素上檢測到以上3個事件,這直接導致對普通DOM元素檢測事件的錯誤,也導致我們可以檢測到window上的事件。好在一般開發者也不會去一個div之類的元素上檢測是否有unload事件。因此補充hasEvent函數,將window上的事件導向一個div對象來檢測部分事件:

if (!supported) { 
if (!element.setAttribute || !element.removeAttribute) { 
element = document.createElement('div'); 
} 
element.setAttribute(name, 'return;'); 
supported = typeof element[name] == 'function'; 
element.removeAttribute(name); 
}

至此,一個較為完整的hasEvent函數完成了,雖然在Firefox上還存在一些問題,比如以下的代碼:

alert(hasEvent('unload', document.createElement('div')); //Firefox彈出true

但是在99%的應用場合之下,這個函數是可以正確的工作的。

添加緩存

為了進一步提高hasEvent的工作效率,考慮到DOM規范規定的事件數量不多,可以對通用的事件(即不指定檢測的元素對象)檢測添加緩存機制。

添加了緩存之後,最終完整的hasEvent函數如下:

var hasEvent = (function () { 
var tags = { 
onsubmit: 'form', onreset: 'form', 
onselect: 'input', onchange: 'input', 
onerror: 'img', onload: 'img', onabort: 'img' 
}, 
cache = {}; 

return function(name, element) { 
name = name.indexOf('on') ? 'on' + name : name; 
//命中緩存 
if (!element && name in cache) { 
return cache[name]; 
} 
element = element || document.createElement(tags[name] || 'div'); 
var proto = element.__proto__ || {}, 
supported = name in element, 
temp; 
//處理顯示在元素的__proto__上加屬性的情況 
if (supported && (temp = proto[name]) && delete proto[name]) { 
supported = name in element; 
proto[name] = temp; 
} 
//處理Firefox不給力的情況 
//Firefox下'onunload' in window是false,但是div有unload事件(OTL) 
if (!supported) { 
if (!element.setAttribute || !element.removeAttribute) { 
element = document.createElement('div'); 
} 
element.setAttribute(name, 'return;'); 
supported = typeof element[name] == 'function'; 
element.removeAttribute(name); 
} 
//添加到緩存 
cache[name] = supported; 
return supported; 
}; 
})();

Mutation Event

Mutation Event是由DOM Level 2制定的一類特殊的事件,這些事件在某個元素為根的DOM樹結構發生變化時觸發,可以在這裡看到具體的事件列表。

遺憾的是hasEvent函數無法檢測到Mutation Event,因此對於此類事件,需要另一種較為復雜的事件檢測方案。

從Mutation Event的列表中可以發現,此類事件的特點在於當DOM樹結構發生變化時才會被觸發,因此可以使用下面這套邏輯去檢測:

  1. 准備一個標記位,默認為false。
  2. 創建出一個DOM樹結構。
  3. 注冊一個Mutation Event。
  4. 通過一定手段讓這個DOM樹變化,從而觸發注冊的事件。
  5. 在事件處理函數中,將標記位設為true。
  6. 返回標記位。

具體的實現代碼可以如下:

function hasMutationEvent(name, tag, change) { 
var element = document.createElement(tag), 
supported = false; 
function handler() { 
supported = true; 
}; 
//IE9開始支持addEventListener,因此只有IE6-8沒有這個函數 
//但是IE6-8已經確定不支持Mutation Event,所以有這個判斷 
if (!element.addEventListener) { 
return false; 
} 
element.addEventListener(name, handler, false); 
change(element); 
element.removeEventListener(name, handler, false); 
return supported; 
};

例如需要檢測DOMAttrModified事件是否存在,只需要用以下代碼:

var isDOMAttrModifiedSupported = 
hasMutationEvent('DOMAttrModified', 'div', function (div) { div.id = 'new'; });

對於其他事件的檢測,同樣只需要制作出一個特定的change函數即可。

DOMContentLoaded

這個事件在文檔加載完成時觸發,但不需要等待圖片等資源下載,多數Javascript框架的document.ready都會試圖使用這個事件。

無論是hasEvent函數還是hasMutationEvent函數都無法檢測到這個事件,但是問題不大,因為:

  1. 這事件和onload一樣,頁面的生命周期中只會觸發一次,不會頻繁使用。
  2. 所有支持addEventListener的浏覽器都支持這個事件(包括IE9),因此判斷簡單。

所以這個事件被排除在了本文討論范圍之外,具體的可以查看各框架的document.ready函數的實現方式。

相關資源

  • Detecting event support without browser sniffing為本文提供了大量的思路。
  • Diego Perini's NWMatcher提供了Mutation Event檢測的思路。
  • 點此查看hasEvent和hasMutationEvent的源碼。

哪位無聊就把所有的Mutation Event的檢測函數寫出來吧……

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved