標準參考
根據 W3C DOM 2 Events 描述,EventTarget 介面被所有支援 DOM 事件模型的節點(Node)實現。 該介面提供了 'addEventListener' 和 'removeEventListener' 方法,用來綁定或解除綁定一個 EventListeners 介面到一個 EventTarget。
DOM 2 Events 中定義了 Event 介面,用來提供事件的上下文資訊,它提供了若干標準屬性和方法。 實現 Event 介面的對象一般作為第一個參數傳入事件處理函數,以用來提供當前事件相關的一些資訊。
DOM 2 事件模型允許 DOM 實現支援事件的多模組,如果有必要,事件模型允許附加新的事件模組。 為了共同使用性的目的,DOM 會定義一個包含低層級的,依賴裝置的事件模組、一個 UI 邏輯事件模組和一個文檔變化事件模組的 UI 事件模組。 第三方實現(比如瀏覽器廠商)在定義一個新類型事件的時候,事件名稱一定不能使用大小寫無關的 'DOM' 字串作開頭,該首碼是為未來的 DOM 事件模組保留的。
DOM 2 已經定義了一個 UI 事件類型模組和一個滑鼠事件類型的模組,它們分別對應 UIEvent 和 MouseEvent 介面。 這兩個介面提供了若干標準屬性和方法,以獲知事件發生時的一些資訊。
關於 EventTarget 介面的詳細資料,請參考 DOM 2 Events 1.3. Event listener registration。
關於 Event 介面的詳細資料,請參考 DOM 2 Events 1.4. Event interface。
關於事件模組的詳細資料,請參考 DOM 2 Events 1.6. Event module definitions。
問題描述
各瀏覽器對元素繫結、解除綁定事件監聽器的方法,事件對象的擷取,以及 Event 對象的實現上存在差異。
造成的影響
如果使用某瀏覽器特有的事件相關的屬性及方法編寫代碼,則可能造成相容性問題,導致代碼報錯,功能失效。
受影響的瀏覽器
問題分析
1. 只有 IE Opera 支援使用 'attachEvent' 和 'detachEvent' 方法綁定和解除綁定事件監聽器
根據 DOM 2 Events 中描述,節點使用 'addEventListener' 和 'removeEventListener' 方法綁定和解除綁定事件監聽器,但 IE6 IE7 IE8 不支援這兩個方法, 而使用 'attachEvent' 和 'detachEvent' 方法作為替代方案,Opera 兩類方法都支援。Chrome Safari Firefox 只支援標準方法。
分析以下代碼:
<button id="add" type="button">add event listener test</button><button id="remove" type="button">remove event listener test</button><div id="info"></div><script type="text/javascript">var add = document.getElementById("add"),remove = document.getElementById("remove"),showMsgA = function(){showMsg("attachEvent")},showMsgB = function(){showMsg("addEventListener")};if(add.attachEvent){add.attachEvent("onclick",showMsgA);remove.attachEvent("onclick",removeE);}if(add.addEventListener){add.addEventListener("click",showMsgB,false);remove.addEventListener("click",removeE,false);}function removeE(){if(add.detachEvent){add.detachEvent("onclick",showMsgA);showMsg("detachEvent");}if(add.removeEventListener){add.removeEventListener("click",showMsgB,false);showMsg("removeEventListener");}}function showMsg(method){document.getElementById("info").innerHTML += ("support " + method + "<br />");}</script>
依次點擊 'add event listener test' >> 'remove event listener test' >> 'add event listener test',測試各瀏覽器對這些方法的支援,結果如下:
| IE6 IE7 IE8 |
![]() |
| Opera |
![]() |
| Chrome Safari Firefox |
![]() |
關於 'addEventListener' 和 'attachEvent' 有幾點需要注意:
- IE 不支援在捕獲階段觸發事件監聽器,'attachEvent' 方法沒有提供參數說明是否響應在捕獲階段觸發的事件;
- 'addEventListener' 和 'attachEvent' 都可以註冊多個事件監聽器;
- 在 Firefox Chrome Safari Opera 中給同一事件註冊同一個事件監聽器多次,重複註冊的會被丟棄;而在 IE 中重複註冊的事件監聽器會被重複執行多次;
- 當給同一元素註冊了多個事件監聽器的時候,IE6 IE7 的事件監聽器執行順序是隨機的,IE8 是倒序的,Firefox Chrome Safari Opera 是順序的;
- 當元素註冊的事件監聽器中有非法的事件監聽器時(非函數),在 IE Firefox 中會拋出異常,而在 Chrome Safari Opera 中則會忽略非法的事件監聽器,繼續執行其他的事件監聽器。
2. 各瀏覽器擷取事件對象的差異
DOM 2 Events 中規定使用事件監聽器的第一個參數作為事件對象,而 IE Opera Chrome Safari 還支援通過 window.event 擷取事件對象。
分析以下代碼:
<button type="button" id="attach">attachEvent</button><button type="button" id="addE">addEventListener</button><button type="button" id="byclick">onClick</button><br />INFO :<div id="info"></div><script type="text/javascript">function $(id){return document.getElementById(id);}var attach = $("attach"),addE = $("addE"),byClick = $("byclick");attach.attachEvent && attach.attachEvent("onclick", handler);addE.addEventListener && addE.addEventListener("click", handler, false);byClick.onclick = handler;function handler(){var src = window === this ? window.event.srcElement : this, type = src.innerHTML;window.event && showMsg(window.event.type, type + " + window.event");arguments[0] && showMsg(arguments[0].type, type + " + arguments[0]");}function showMsg(type, msg){$("info").innerHTML += (msg + "(type : " + type + ")<br />");}</script>
以上程式碼群組合不同的事件監聽器綁定方式和事件對象擷取方法,測試各瀏覽器的支援程度。
依次點擊 'attachEvent' >> 'addEventListener' >> 'onClick',結果如下:
| IE6 IE7 IE8 |
![]() |
| Chrome Safari |
![]() |
| Opera |
![]() |
| Firefox |
![]() |
匯總測試結果如下表:1
| 事件對象擷取方式 |
IE6 IE7 IE8 |
Chrome Safari |
Opera |
Firefox |
| window.event |
Y |
Y |
Y |
N |
| arguments[0] |
Y2 |
Y |
Y |
Y |
注1: Y 表示支援該事件對象擷取方式,N 表示不支援。
注2: 部分支援。
從上表出可以看出:
- 只有在使用 attachEvent 方法註冊事件監聽器的時候,IE 才支援使用事件監聽器傳入的第一個參數作為事件對象的方式;
- Chrome Safari Opera 兩種擷取事件對象的方式都支援;
- Firefox 只支援擷取事件對象的標準方式。
3. 各瀏覽器中事件對象的屬性和方法的差異
IE 對事件對象的標準屬性和方法支援有限,針對大部分屬性和方法,IE 都提供了一套替代非標準的替代方案; 而 Firefox Chrome Safari Opera 除了全面支援事件對象的標準屬性和方法外,還在不同程度上支援了 IE 提供的非標準替代方案。
以下代碼檢測了 Event、UIEvent、MouseEvent 介面以及事件對象的非標準屬性在各瀏覽器下的支援程度:
<button type="button" id="iEvent">Interface Event</button><button type="button" id="iUIMouse">Interface UIEvent & MouseEvent</button><input type="text" id="nosK" /><p id="wrap" style="border:3px solid;padding:5px;width:500px;"><a id="nosCM" href="#"><img src="google.gif" alt="IE"/></a></p><br /><table border="1"><tbody><tr><th>Interface Event</th><td id="einfo"></td></tr><tr><th>Interface UIEvent<br/>&<br/>MouseEvent</th><td id="minfo"></td></tr><tr><th>Non-standard<br/>&<br/>click</th><td id="ncinfo"></td></tr><tr><th>Non-standard<br/>&<br/>mouseover mouseout</th><td id="nminfo"></td></tr><tr><th>Non-standard<br/>&<br/>keyCode</th><td id="nkinfo"></td></tr></tbody></table><script type="text/javascript">function $(id){return document.getElementById(id);}function addEvent(elem, type, handler, useCapture){elem.addEventListener ? elem.addEventListener(type, handler, useCapture) :elem.attachEvent("on" + type, handler);}addEvent($("iEvent"), "click", handleEvent, false);addEvent($("iUIMouse"), "click", handleUIMouse, false);addEvent($("nosCM"), "click", handleNoSClick, false);addEvent($("wrap"), "click", function(){alert("P tag.");}, false);addEvent($("nosCM"), "mouseover", handldNoSMouse, false);addEvent($("nosCM"), "mouseout", handldNoSMouse, false);addEvent($("nosK"), "keydown", handleNoSKey, false);addEvent($("nosK"), "keypress", handleNoSKey, false);addEvent($("nosK"), "keyup", handleNoSKey, false);function handleEvent(e){e = e || window.event;var props = {type : "type",target : "target",currentTarget : "currentTarget",eventPhase : "eventPhase",bubbles : "bubbles",cancelable : "cancelable",timeStamp : "timeStamp",initEvent : "initEvent",preventDefault : "preventDefault",stopPropagation : "stopPropagation"};showMsg(props, e, $("einfo"));}function handleUIMouse(e){e = e || window.event;var props = {view : "view",detail : "detail",initUIEvent : "initUIEvent",screenX: "screenX",screenY : "screenY",clientX : "clientX",clientY : "clientY",ctrlKey : "ctrlKey",shiftKey : "shiftKey",altKey : "altKey",metaKey : "metaKey",button : "button",relatedTarget : "relatedTarget",initMouseEvent : "initMouseEvent"}showMsg(props, e, $("minfo"));}function handleNoSClick(e){e = e || window.event;e.returnValue = false;e.cancelBubble = true;var props = {cancelBubble : "cancelBubble",offsetX : "offsetX",offsetY : "offsetY",returnValue : "returnValue",srcElement : "srcElement",x : "x",y : "y"};showMsg(props, e, $("ncinfo"));}function handldNoSMouse(e){e = e || window.event;var props = {fromElement : "fromElement",toElement : "toElement"};showMsg(props, e, $("nminfo"));}function handleNoSKey(e){e = e || window.event;$("nkinfo").innerHTML += "<strong>" + e.type + "</strong> : " + e.keyCode + "<br/>";}function showMsg(props, e, info){var tmp = "", p, val;with(e){for(p in props) {try{val = eval(props[p]) + " [" + typeof eval(props[p]) + "]";} catch (e) {val = undefined;}tmp += "<strong>" + p + "</strong> : " + val + "<br />"}}info.innerHTML = tmp;}</script>
執行以上測試代碼,分別點擊 'Interface Event' 按鈕、'Interface UIEvent & MouseEvent' 按鈕和圖片,滑鼠移到圖片上再移開,在文字框中輸入 'a',得到結果整理如下表:3
| Interface & Non-standard |
Property & method |
IE6 IE7 IE8 |
Chrome Safari Opera |
Firefox |
| Interface Event |
type |
Y |
Y |
Y |
| target |
N |
Y |
Y |
| currentTarget |
N |
Y |
Y |
| eventPhase |
N |
Y |
Y |
| bubbles |
N |
Y |
Y |
| cancelable |
N |
Y |
Y |
| timeStamp |
N |
Y |
Y |
| initEvent |
N |
Y |
Y |
| preventDefault |
N |
Y |
Y |
| stopPropagation |
N |
Y |
Y |
| Interface UIEvent |
view |
N |
Y |
Y |
| detail |
N |
Y |
Y |
| initUIEvent |
N |
Y |
Y |
| Interface MouseEvent |
screenX |
Y |
Y |
Y |
| screenY |
Y |
Y |
Y |
| clientX |
Y |
Y |
Y |
| clientY |
Y |
Y |
Y |
| ctrlKey |
Y |
Y |
Y |
| shiftKey |
Y |
Y |
Y |
| altKey |
Y |
Y |
Y |
| metaKey |
N |
Y |
Y |
| button |
Y |
Y |
Y |
| relatedTarget |
N |
Y |
Y |
| initMouseEvent |
N |
Y |
Y |
| Non-standard |
cancelBubble |
Y |
Y |
Y |
| offsetX |
Y |
Y |
N |
| offsetY |
Y |
Y |
N |
| returnValue |
Y |
Y |
N |
| srcElement |
Y |
Y |
N |
| x |
Y |
Y |
N |
| y |
Y |
Y |
N |
| fromElement |
Y |
Y |
N |
| toElement |
Y |
Y |
N |
| keyCode |
Y |
Y |
Y |
注3: Y 代表事件對象支援該屬性或方法,N 代表不支援。
從上表中可以看出:
- IE 支援事件對象的所有非標準屬性,不支援除 'type' 外 Event 介面的所有方法屬性及方法,不支援 UIEvent 介面的所有屬性和方法,不支援 MouseEvent 介面的 'metaKey'、'relatedTarget' 屬性和 'initMouseEvent' 方法;
- Chrome Safari Opera 支援事件對象的所有標準及非標準的屬性和方法;
- Firefox 支援事件對象的所有標準屬性和方法,但僅支援非標準屬性中的 'cancelBubble' 和 'keyCode'。
需要注意的是:
- Firefox 不支援事件對象的 'returnValue' 屬性,測試範例中雖然顯示 'returnValue' 值為 false,但這僅僅是因為給事件對象添加了 'returnValue' 屬性,並沒有起到取消事件預設動作的作用,這從地址欄可以看出,多了 '#' 號,這是 A 標籤的 'href' 屬性造成的。
- 各瀏覽器對 Event 介面的 'timeStamp' 屬性傳回值都不同。
關於 IE 實現的事件對象非標準的屬性及方法的詳細資料,請參考 MSDN event Object。
關於 Firefox 對事件對象實現的詳細資料,請參考 MDC event。
解決方案
1. 使用特性判斷建立無相容性問題的事件監聽器綁定和解除綁定函數
如:
function addEvent(elem, type, handler, useCapture){elem.addEventListener ? elem.addEventListener(type, handler, useCapture) :elem.attachEvent("on" + type, handler);}function removeEvent(elem, type, handler, useCapture){elem.removeEventListener ? elem.removeEventListener(type, handler, useCapture) :elem.detachEvent("on" + type, handler);}
2. 使用特性判斷獲得有效事件對象
在事件監聽器中判斷傳入的第一個參數或 window.event 是否有效,如:
function handler(e){e = e || window.event;}
3. 使用特性判斷使用與標準對應的非標準方法及屬性
儘管 IE 對事件對象的標準屬性和方法支援有限,但它自己實現的事件模型也基本能夠替代或實現標準屬性或方法的功能。
下表總結了部分與標準事件對象對應的,或可以實現標準屬性或方法功能的非標準屬性:
| Standard |
Non-standard |
| target |
srcElement |
| preventDefault() |
returnValue |
| stopPropagation() |
cancelBubble |
| relatedTarget |
fromElement toElement |
另,標準的 'clientX' 屬性和非標準的 'x' 屬性作用是相同的,同樣 'clientY' 和 'y' 也是。