再看這篇博客之前,希望你已經對js高級程序編程一書中的事件模塊進行了詳讀,不然我只能呵呵了。
document.createEventObject,在IE下創建事件對象event。
elem.fireEvent,在IE下觸發事件,里面有兩個參數type,event。其中type是觸發的事件類型,event是事件本身。舉個例子:
document.attachEvent('ondataavailable', function (event) { //document上綁定自定義事件dataavailable
alert(event.eventType);
});
var event = document.createEventObject(); //調用document對象的createEventObject方法得到一個event的對象實例。
event.eventType = 'message';
document.fireEvent('ondataavailable', event); //觸發document上綁定的自定義事件dataavailable
這時就會執行function,並把event對象實例傳入方法中,打印出message。
以上是IE操作的方式,W3C的方式:
document.addEventListener('dataavailable', function (event) { //document上綁定自定義事件dataavailable
alert(event.eventType);
}, false);
var event = document.createEvent('HTMLEvents'); //調用document對象的 createEvent 方法得到一個event的對象實例。
event.initEvent("dataavailable", true, true);
// initEvent接受3個參數:
// 事件類型,是否冒泡,是否阻止瀏覽器的默認行為
event.eventType = 'message';
document.dispatchEvent(event); //觸發document上綁定的自定義事件ondataavailable
W3C要用三個方法:document.createEvent();接受一個參數,創建的事件類型,理應是Events,但是標准瀏覽器支持HTMLEvents。event.initEvent();接收三個參數,第一個是事件類型,第二個是否冒泡,第三個是否阻止瀏覽器的默認行為。elem.dispatchEvent(),接收一個參數,創建的event的對象實例。
兼容性方法的寫法:
function fireEvent(elem , type , args ){ //第一個參數代表要觸發的事件綁定的元素,第二個參數代表要觸發的事件類型,第三個是需要傳入event對象實例的屬性值 args = args || {}; //比如傳入{name:"chaojidan",eventName:"zidingyi"} var event; if(elem.dispatchEvent){ event = document.createEvent("HTMLEvents"); event.initEvent(type,true,true); }else{ event = document.createEventObject(); } for(var i in args){ event[i] = args[i]; } if(elem.dispatchEvent){ elem.dispatchEvent(event); }else{ elem.fireEvent("on"+type, event); } }
onXXX綁定方式的缺陷:
(1)對DOM3新增事件或火狐某些私有實現無法支持,比如,DOMContentLoaded事件,DOMMouseScroll事件(此事件用於火狐模擬其他瀏覽器的mousewheel事件,火狐沒有此事件)。
(2)onXXX只允許元素每次綁定一個回調,重復綁定,會覆蓋。
(3)在IE下,回調方法沒有參數(需要通過window.event),其他瀏覽器下回調的第一個參數是事件對象。
(4)只能在冒泡階段可用。
attachEvent綁定方式的缺陷:IE9就開始支持W3C的綁定方式了。
(1)只支持微軟系的事件,DOM3的事件不能用,比如:DOMContentLoaded事件
(2)回調中的this,不是指向被綁定元素,而是window。
(3)綁定多個回調方法時,觸發時,並不是按照綁定時的順序依次觸發。
(4)只支持冒泡階段。
addEventListener綁定方式的缺陷:
(1)火狐不支持focusin,focus,DOMFocusIn,DOMFocusOut事件,而且直到現在都不願意用mousewheel代替DOMMouseScroll。Chrome不支持mouseenter和mouseleave。因此標准瀏覽器雖然支持這種方式綁定事件,但是支持的事件類型不一樣。國內一些瀏覽器套用webkit內核,為了使自己的瀏覽器跑分高,竟然實現了一些無用 的空接口來騙過特征偵測,因此有時需要使用功能偵測來檢測瀏覽器是否支持此事件。
(2)此方法還有第四,第五個參數。第四個參數是火狐專有實現,允許跨文檔監聽事件。第5個參數只存在flash語言的同名方法中。在Flash下,addEventListener的第四個參數用於設置該回調執行時的順序,數字大的優先執行,第5個參數用於指定對偵聽器函數的引用是弱引用還是正常引用。(知道就行,不需要深究)
(3)事件對象成員的不穩定。比如:safari下,event.target可能返回文本節點。event.defaultPrevented代表事件對象有沒有調用preventDefault方法。這里有很多奇葩的成員屬性,不需要深究。
最后講一下Dean Edward的addEvent.js源碼分析,這是jQuery事件系統的源頭。早期的一個事件系統。
function addEvent(element, type, handler){ //元素element,事件類型type,綁定事件處理方法handler。給元素element綁定type的事件類型,事件處理方法是handler
if(!handler.$$guid) { //判斷處理方法handler是否有$$guid屬性,沒有就進入if語句
handler.$$guid = addEvent.guid ++ ; //addEvent.guid =1;每次綁定一個新的事件處理方法,都會加1,一個唯一的值。
}
if(!element.events){ //如果元素沒有events屬性,就進入if語句
element.events = {};
}
var handlers = element.events[type]; //給element.events對象添加事件類型type的屬性
if(!handlers){ //如果沒有element.events[type]不存在,就進入if語句,第一次執行時,是undefined,所以進入if語句
handlers = element.events[type] = {};
if(element["on"+type]){ //如果元素之前用onXXX方式綁定過此type事件,就進入if語句
handlers[0] = element["on"+type]; //把用onXXX綁定的事件處理函數fn賦給element.events[type][0],其實就是element.events[type] ={0:fn}
}
}
handlers[handler.$$guid] = handler; //把事件處理方法handler添加到handler對象中,其實就是element.events[type] = {1: handler},如果此元素element之前通過onXXX的方式綁定過fn,那么這時應該是element.events[type] = {0:fn , 1: handler}
element["on"+type] = handleEvent; //給元素element綁定type類型的事件,只要在元素element觸發了type類型的事件,就會調用handleEvent方法,此方法就會調用element.events[type] 對象中的屬性方法。
}
function handleEvent(event){
event = event || window.event; //IE瀏覽器,通過onXXX綁定的事件處理函數,接收不到event對象,只能通過window.event取到。
var handlers = this.events[event.type]; //this指的是element(事件綁定的元素),event.type是事件觸發的類型type。其實就是上面的element.events[type]對象
for(var i in handlers){
this.$$handleEvent = handlers[i]; //取到element綁定type事件的事件處理函數,其實就是上面的fn和handler方法賦給element的$$handleEvent屬性
this.$$handleEvent(event) ; //按照綁定事件處理函數的順序,執行。先執行fn,在執行handler。並傳入已做了兼容性的event對象
}
}
以上有一個bug,就是event的取值,如果是在iframe中點擊事件時,傳進來的window就是iframe的window了。需要改成如下格式:
event = event || ((this.ownerDocument || this.document || this).parentWindow || window).event;
this指的是觸發事件的元素,如果元素的document有parentWindow屬性,那么就證明是在iframe中觸發的事件,因此需要取它的父窗口。觸發事件的元素可能是document本身,或者是window本身,因此加了后面兩個對象this.document(window)和this(document)。因為只有document有parentWindow屬性。如果是在最外層window觸發的,它的document.parentWindow會返回false,因此直接用window.event。
以上的addEvent方法,在交錯引用時,會產出內存泄露(js對象引用DOM元素節點,同時DOM元素節點也引用js對象),因此,建議給元素就分配一個UUID,所有的回調都放到一個js對象存儲(不再放在element的屬性對象中,這會導致DOM元素節點引用js對象)。代碼如下:
addEvent.handlers = {}
function addEvent(element, type, handler){ //元素element,事件類型type,綁定事件處理方法handler。給元素element綁定type的事件類型,事件處理方法是handler
if(!handler.$$guid) { //判斷處理方法handler是否有$$guid屬性,沒有就進入if語句
handler.$$guid = addEvent.guid ++ ; //addEvent.guid =1;每次綁定一個新的事件處理方法,都會加1,一個唯一的值。
}
if(!element.$$guid) { //判斷元素element是否有$$guid屬性,沒有就進入if語句
element.$$guid = addEvent.guid ++ ; //這里element.$$guid=2,一個唯一的值,如果之前此元素已經綁定過事件,它就不會進入if語句時。
}
if(!addEvent.handlers[element.$$guid]){//addEvent.handlers是一個對象,它判斷此對象里面是否有此元素對應的唯一的值的屬性,其實就是:addEvent.handlers[2]
addEvent.handlers[element.$$guid] = {}; //如果沒有,就進入if語句,addEvent.handlers[2] = {};
}
var handlers = addEvent.handlers[element.$$guid][type]; //addEvent.handlers[2][type],第一次綁定時,此type屬性值不存在。
if(!handlers){ //如果沒有此type屬性值,就進入if語句,第一次執行時,是undefined,所以進入if語句
handlers = addEvent.handlers[element.$$guid][type] = {};
if(element["on"+type]){ //如果元素之前用onXXX方式綁定過此type事件,就進入if語句
handlers[0] = element["on"+type]; //把用onXXX綁定的事件處理函數fn賦給addEvent.handlers[element.$$guid][type] = {0:fn}
}
}
handlers[handler.$$guid] = handler; //其實就是addEvent.handlers[element.$$guid][type]= {1: handler},如果此元素element之前通過onXXX的方式綁定過fn,那么這時應該是addEvent.handlers[element.$$guid][type] = {0:fn , 1: handler}
element["on"+type] = handleEvent;
}
以上代碼的改進,其實就是給元素element分配了一個唯一的值(UUID),同時把所有的回調方法,都放到addEvent.handlers對象中進行處理了,而不是放到元素element的屬性對象中。
jQuery的事件系統就是通過改進上面的代碼實現的,它其中綁定事件不使用onXXX的模式(會存在內存泄露),而是使用addEventListener和attachEvent綁定事件。每個元素只綁定一次就OK了,減少了DOM操作。
加油!