第二十二課:js事件原理以及addEvent.js的詳解


再看這篇博客之前,希望你已經對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操作。

 

 

 

加油!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM