先看看下面一道題目,請評價以下代碼並給出改進意見:
if (window.addEventListener) {//標准瀏覽器 var addListener = function(el, type, listener, useCapture) { el.addEventListener(type, listener, useCapture); }; } else if (document.all) {//IE addListener = function(el, type, listener) { el.attachEvent("on" + type, function() { listener.apply(el); }); } }
1)不應該在if和else語句中聲明addListener函數,應該先聲明;
2)不需要使用window.addEventListener或document.all來進行檢測瀏覽器,應該使用能力檢測;
3)attachEvent在IE中有this指向問題,會指向window,雖然上面的代碼做了指向處理,但是匿名函數不能做detachEvent解綁
改進后的代碼稍后加上。
一、冒泡與捕獲
使用過addEventListener方法的會發現最后一個參數“useCapture”,用於控制是捕獲還是冒泡。
何為冒泡、捕獲,請看下面的例子:查看在線代碼,在線代碼中用了三層。
<div id="click1"> <div id="click2">事件</div> </div>
1)Netscape主張元素1的事件首先發生,這種事件發生順序被稱為捕獲型,如下圖所示:
| | ---------------| |----------------- | click1 | | | | -----------| |----------- | | |click2 \ / | | | ------------------------- | | Event CAPTURING | -----------------------------------
2)微軟則保持元素2具有優先權,這種事件順序被稱為冒泡型,如下圖所示:
/ \ ---------------| |----------------- | click1 | | | | -----------| |----------- | | |click2 | | | | | ------------------------- | | Event BUBBLING | -----------------------------------
3)W3C選擇了一個擇中的方案。任何發生在w3c事件模型中的事件,首是進入捕獲階段,直到達到目標元素,再進入冒泡階段,如下圖所示:
| | / \ ---------------| |--| |------------ | click1 | | | | | | -----------| |--| |------ | | |click2 \ / | | | | | ------------------------- | | W3C event model | -----------------------------------
4)阻止冒泡,很多時候是不想觸發父級的相同事件的,那么就需要阻止這種行為。
W3C方:event.stopPropagation()。(chrome、firefox、safrai等)
IE方:event.cancelBubble設置為true。
經過我的在線測試,stopPropagation這個方法不能阻止捕獲。
這里順帶說下阻止默認事件的方法,何為默認事件?就比如a標簽設置了href,就會做跳轉,這里阻止它跳轉。
W3C方:event.preventDefault(),但只有event的cancelable屬性為true時才能使用。
IE方:event.returnValue設置為false。
二、事件系統
瀏覽器提供了3種層次的API。
1)最原始的是寫在元素標簽內
2)以el.onXXX=function綁定的方式,通稱為DOM0事件系統。
3)一個元素的同一類型事件可以綁定多個回調,通稱為DOM2事件系統。
IE與W3C依舊不同,語法如下:
| 序號 | 操作與對象 | IE方 | W3C方 |
| 1 |
綁定事件 |
el.attachEvent("on"+type, callback) | el.addEventListener(type,callback,[useCapture]) |
| 2 |
卸載事件 |
el.detachEvent("on"+type,callback) | el.removeEventListener(type,callback,[useCapture]) |
| 3 | 創建事件 | document.createEventObject() | document.createEvent(types) 創建事件(過時) event.initEvent() 初始化事件(過時) new Event(types) |
| 4 |
派發事件 |
el.fireEvent(type,event) | el.dispatchEvent(event) |
| 5 | event屬性 | srcElement:等於target,默認目標 | currentTarget:其事件處理程序當前正處理事件的元素 target:事件的目標 |
| 6 | event方法 | returnValue:等於preventDefault() cancelBubble:設為true等於stopPropagation() |
preventDefault():阻止默認行為 stopPropagation():阻止冒泡 |
| 7 |
type |
被觸發的事件類型,需要“on”前綴 | 被觸發的事件類型 |
| 8 |
事件執行順序 |
與添加順序相反 | 與添加順序一致 |
| 9 | 匿名函數 |
無法移除 | 無法移除 |
| 10 |
this |
window | 當前綁定的元素 |
注意第8點,在測試代碼中,我綁定了兩個相同的“click”事件, 查看在線完整代碼。
var func1 = function(e) { alert(1); //測試執行順序 }; var func2 = function(e) { alert(2); }; var type = 'click'; bind(ele, type, func1); bind(ele, type, func2);
在IE中先彈出2,再彈出1。而在chrome中先彈出1,再彈出2。
注意上面的第9點和第10點,上面那道題目中要解決的就是這個問題。下面的代碼是個片段,查看在線完整代碼。
var bind = function(ele, type, callback) { if (!ele[type + "event"]) { ele[type + "event"] = {}; //聲明一個空對象,緩存事件 } var name = callback.toString(); if (!ele[type + "event"][name]) { var handler = function(event) { //可以做更多event封裝操作 var ev = event || window.event; callback.call(ele, ev); }; ele[type + "event"][name] = handler; //做個臨時變量 } if (ele.addEventListener) { ele.addEventListener(type, handler, false); } else if (ele.attachEvent) { ele.attachEvent('on' + type, handler); } } var unbind = function(ele, type, callback) { var handler = ele[type + "event"][callback.toString()]; //讀取臨時變量 if (ele.removeEventListener) { ele.removeEventListener(type, handler); } else if (ele.detachEvent) { ele.detachEvent('on' + type, handler); } }
上面的代碼還比較粗糙,僅僅是用於演示一下。方法有很多,自己可以揣摩。
我看到網上有人直接不用attachEvent,將相應的事件保存在一個數組中,當detachEvent的時候做splice數組的操作。
大家也可以參考下一些成熟的類庫,例如jQuery1.8.3版本,2642行的event.add封裝,2757行的event.remove封裝,3485行的on封裝。
也可以參考Dean Wdwards寫的addEvent方法,這是Prototype時代早期出現的一個事件系統,jQuery事件系統的源頭。
參考資料:
