jQuery事件系統詳解

積木村の研究所發表於2015-12-13

我們習慣理所當然的使用jQuery提供的高階事件API,jQuery幫我們處理了事件序號產生器制在各個瀏覽器間的相容性,幫我們實現了高階的事件委託機制,大大的提高了我們的工作效率;然而我從來都不是一個知其然不知其所以然的工程師,你是否一樣?如果你同樣對其實現細節感興趣,請仔細閱讀本文。本文不涉及jQuery提供的自定義事件。

我將本文組織為如下結構:

1.使用原生API有什麼問題
2.大牛是如何解決的
3.jQuery的實現

1. 使用原生API有什麼問題

(1) 最原始的方法是直接對dom元素的onclick、onmouseover等屬性賦值一個函式:

Element.onclick = function(){
   //event handler logic
};

然而這種方式一次只能新增一個事件處理函式,不具有通用性。

(2) 對於符合最新W3C標準的瀏覽器(IE>8,chrome,firefox,android)可以使用element.addEventListener,將事件型別作為一個引數傳遞。

element.addEventListener(‘click’,function(){
   //event handler logic
},false);

element.addEventListener是支援新增多個事件的,呼叫順序是事件新增順序。並且我們可以通過傳遞第三個引數來控制事件的觸發時機是捕獲階段還是冒泡階段,預設為冒泡階段。然而低版本的IE(<=8)不支援element.addEventListener。

(3)對於低版本的IE(<=8)相應的提供了element.attachEvent:

element.attachEvent(‘onclick’,function(){
        //event handler
});

element.attachEvent的事件型別引數格式必須是on+事件型別,但是監聽函式內部的this指標指向的卻不是觸發事件的element。

由此可見,以上三種方法直接使用原生API的方法都是有問題的或者說不完美的。


2.Dean Edwards大牛的實現

Dean Edwards在2005年寫的了addevent的庫,用於處理對瀏覽器的相容性問題。理解了這個addEvent庫,我們可以更好的理解jQuery的事件系統。

//事件新增方法
addEvent.guid = 1;
function addEvent(element, type, handler) {
    // 為傳入的每個事件初始化一個唯一的id
    if (!handler.$$guid) handler.$$guid = addEvent.guid++; //下文:addEvent.guid = 1;

    // 給element維護一個events屬性,初始化為一個空物件。  
    // element.events的結構類似於 { "click": {...}, "dbclick": {...}, "change": {...} }  
    // 即element.events是一個物件,其中每個事件型別又會對應一個物件
    if (!element.events) element.events = {};

    // 試圖取出element.events中當前事件型別type對應的物件,賦值給handlers
    var handlers = element.events[type];
    if (!handlers) {
        handlers = element.events[type] = {};
        //如果handlers是undefined,則初始化為空物件
        // 如果這個element已經有了一個方法,例如已經有了onclick方法
        // 就把element的onclick方法賦值給handlers的0元素,此時handlers的結構就是:
        // { 0: function(e){...} }
        // 此時element.events的結構就是: { "click": { 0: function(e){...} },  /*省略其他事件型別*/ } 
        if (element["on" + type]) {
            handlers[0] = element["on" + type];
        }
    }
    // 把當前的事件handler存放到handlers中,handler.$$guid = addEvent.guid++; addEvent.guid = 1; 肯定是從1開始累加的
    // 因此,這是handlers的結構就是 { 0: function(e){...}, 1: function(){}, 2: function(){} 等等... }
    handlers[handler.$$guid] = handler;
    // 下文定義了一個handleEvent(event)函式
    // 將這個函式,繫結到element的type事件上。  說明:在element進行click時,將會觸發handleEvent函式,handleEvent函式將會查詢element.events,並呼叫相應的函式。可以把handleEvent稱為“主監聽函式”
    element["on" + type] = handleEvent;
};


function handleEvent(event) {
    // 在IE中,event需要通過window.event獲取
    event = event || window.event;
    // 根據事件型別在events中獲取事件集合(events的資料結構,參考addEvent方法的註釋)
    var handlers = this.events[event.type];
    // 注意!注意!  這裡的this不是window,而是element物件,因為上文 element["on" + type] = handleEvent;
    // 所以在程式執行時,handleEvent已經作為了element的一個屬性,它的作用域是element,即this === element

    // 迴圈執行handlers集合裡的所有函式    另外,這裡執行事件時傳遞的event,無論在什麼瀏覽器下,都是正確的
    for (var i in handlers) {
        //此處為何要把handlers[i]賦值給this.$$handleEvent
        this.$$handleEvent = handlers[i];
        this.$$handleEvent(event);
    }
};

Dean Edwards的這段程式碼還是比較好理解的,他首先將事件組織成一個如下結構,即每一種事件型別維持一個繫結事件處理函式列表,當某一類事件觸發時,通過一個wrapper函式(通過onEvent API繫結的handleEvent)輪詢對應列表並依次處理。事件列表結構組織如下:

element: {
            onclick: handleEvent(event),   /*下文定義的函式*/
            events: {
                click:{
                    0: function(){...},    /*element已有的click事件*/
                    1: function(){...},
                    2: function(){...}
                    /*.......其他事件......*/
                },
                change:{
                    /*省略*/
                },
                dbclick:{
                    /*省略*/
                }
            }
}

如果理解了以上程式碼,那麼jQuery的事件處理系統就容易懂了。


3. jQuery的實現

其實用Dean Edwards的addEvent庫就可以很好的實現了事件處理程式的健壯性:

 (1).跨瀏覽器相容性
 (2).this變數的正確指向
 (3).支援新增多個事件處理函式

但是jQuery的真正強大之處在於它對事件委託的實現,這也是我們接下來分析的要點,關於事件委託的介紹請參考我的另一篇文章。

對於事件繫結,jQuery提供了一個萬能函式on,$(selector).on(event,childSelector,data,function,map),基本上jQuery的各種事件函式變體都是對on函式的封裝: 比如:$(selector).click()

jQuery.fn[ 'click' ] = function( data, fn ) {
    return arguments.length > 0 ?
         this.on( name, null, data, fn ) :
         this.trigger( name );
};

$(selector).bind ():

bind: function( types, data, fn ) {
    return this.on( types, null, data, fn )
}

還有$(selector).live()$(selector).delegate(),具體各個api的區別請參考jQuery文件。

(1) 解析on函式

on函式內部呼叫了jQuery.event.add的add函式。

handlers: function( event, handlers ) {
  //觸發事件的節點
  cur = event.target;
  //事件列表中委託事件的個數,目前事件列表中存放css選擇器以及對應的事件處理函式
  delegateCount = handlers.delegateCount,
  //針對每一個觸發事件,模擬冒泡過程,匹配到要代理節點,並放到匹配陣列中,this指向委託節點,而cur指向事件觸發節點,從事件觸發節點到委託節點模擬冒泡過程,查詢匹配需要委託選擇器的節點。
  /×
   ×  注意:一個節點可能匹配多個選擇器,相應的也會被觸發多次,而模擬冒泡過程也保證了離event.target比較進的節點放在佇列的最前面
   ×/
    for ( ; cur !== this; cur = cur.parentNode || this ){
         for ( i = 0; i < delegateCount; i++ ){
                //在冒泡過程中,匹配節點
                sel = handleObj.selector + " ";
                matches[ sel ] = handleObj.needsContext ?
                jQuery( sel, this ).index( cur ) >= 0 :
                jQuery.find( sel, this, null, [ cur ] ).length;
                //將匹配到的節點壓入匹配陣列
                matches.push( handleObj );
         }
         //對於代理節點
         if ( matches.length ) {
           //將匹配到的節點和對應的事件處理後設資料列表放到handlerQueue佇列中,以供dispatch使用,注意這裡elem為匹配到的節點(要委託的節點),這是為了保證呼叫委託handler函式的內部this是正確的。
            handlerQueue.push({ elem: cur, handlers: matches });
         }
    }
    // 如果還有不需要代理的事件,則直接放入佇列尾部
    if ( delegateCount < handlers.length ) {
        //注意這裡的elem為this(代理節點),這也保證了非委託handler內部this的正確指向
        handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
    }
}

理解了handlers函式的實現,dispatch函式的邏輯就簡單了:

dispatch: function( event ){
        //獲取handlerQueue佇列
        handlerQueue = jQuery.event.handlers.call( this, event, handlers );
        //對每一個需要觸發的節點,執行相應的處理函式
        while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
               while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
                    //呼叫事件處理函式,這時已經保證了委託函式先呼叫的原則,同時保證了this指向的正確性
                    handleObj.handler.apply( matched.elem, args );
              }
        }
}

沒錯,現在我們基本分析完了jQuery事件系統的實現。可以說jQuery的實現參考了Dean Edwards的程式碼,同時新增了事件委託的實現。

本文同時發表在我的部落格積木村の研究所http://foio.github.io/jquery-event-research/

相關文章