解密jQuery事件核心 - 委托設計(二)


第一篇 http://www.cnblogs.com/aaronjs/p/3444874.html

從上章就能得出幾個信息:

  • 事件信息都存儲在數據緩存中
  • 對於沒有特殊事件特有監聽方法和普通事件都用addEventListener來添加事件了。
  • 而又特有監聽方法的特殊事件,則用了另一種方式來添加事件。

 

本章分析的重點:

通過addEventListener觸發事件后,回調句柄如何處理?

具體來說就是,如何委派事件的,用到哪些機制,我們如果用到項目上是否能借鑒?

 

涉及的處理

事件句柄的讀取與處理

事件對象的兼容,jQuery采取什么方式處理?

委托關系的處理

 

jQuery引入的處理方案

jQuery.event.fix(event):將原生的事件對象 event 修正為一個 可以讀讀寫event 對象,並對該 event 的屬性以及方法統一接口。

jQuery.Event(event,props): 構造函數創建可讀寫的 jQuery事件對象 event, 該對象即可以是原生事件對象 event 的增強版,也可以是用戶自定義事件

jQuery.event.handlers: 用來區分原生與委托事件

 

能學到的思路

緩存的分離

適配器模式的運用

事件兼容性的封裝

委托的設計

 


事件的綁定執行順序

結構

<div id='p1' style="width: 500px;height: 500px;background: #ccc">
    <div id='p2' style="width: 300px;height: 300px;background: #a9ea00">
        <p id="p3" style="width: 100px;height: 100px;background: red" id = "test">
            <a id="p4" style="width: 50px;height: 50px;background:blue" id = "test">點擊a元素</a>
        </p>
    </div>
</div>

 

假如每一個節點都綁定了事件,那么事件的觸發順序如下:

image

由此可見:

默認的觸發循序是從事件源目標元素也就是event.target指定的元素,一直往上冒泡到document或者body,途經的元素上如果有對應的事件都會被依次觸發

 


如果遇到委托處理?

看demo


 

 


最后得到的結論:

元素本身綁定事件的順序處理機制

分幾種情況:

假設綁定事件元素本身是A,委派元素B.C

第一種:

A,B,C各自綁定事件, 事件按照節點的冒泡層次觸發

 

第二種:

元素A本身有事件,元素還需要委派元素B.C事件

委派的元素B.C肯定是該元素A內部的,所以先處理內部的委派,最后處理本身的事件

 

第三種:

元素本身有事件,元素還需要委派事件,內部委派的元素還有自己的事件,這個有點繞

先執行B,C自己本身的事件,然后處理B,C委派的事件,最后處理A事件

 

為什么需要了解這個處理的順序呢? 因為jQuery做委托排序的時候要用到

 


既然可以冒泡,相應的也應該可以停止

事件對象提供了preventDefault,stopPropagation2個方法一個停止事件傳播,一個傳遞默認的行為(暫且無視IE)

jQuery提供了個萬能的 return false 不僅可以阻止事件冒泡,還可以阻止瀏覽器的默認行為,還可以減少ie系列的bug。

其實就是根據返回的布爾值調用preventDefault,stopPropagation方法,下面會提到

e.stopImmediatePropagation方法不僅阻止了一個事件的冒泡,也把這個元素上的其他綁定事件也阻止了

事件委托原理都知道,但是能有多少寫得出jQuery這樣的設計思路呢?好吧,如果您覺得不需要,那么看看總是沒壞處的。。。

先看看jQuery需要應對的幾個問題

 


需要處理的的問題一:事件對象不同瀏覽器的兼容性

event 對象是 JavaScript 中一個非常重要的對象,用來表示當前事件。event 對象的屬性和方法包含了當前事件的狀態。

當前事件,是指正在發生的事件;狀態,是與事件有關的性質,如 引發事件的DOM元素、鼠標的狀態、按下的鍵等等。

event 對象只在事件發生的過程中才有效。

瀏覽器的實現差異:

獲取event對象

  • 在 W3C 規范中,event 對象是隨事件處理函數傳入的,Chrome、FireFox、Opera、Safari、IE9.0及其以上版本都支持這種方式;
  • 但是對於 IE8.0 及其以下版本,event 對象必須作為 window 對象的一個屬性。
  • 在遵循 W3C 規范的瀏覽器中,event 對象通過事件處理函數的參數傳入。
  • event的某些屬性只對特定的事件有意義。比如,fromElement 和 toElement 屬性只對 onmouseover 和 onmouseout 事件有意義。

特別指出:分析的版本是2.0.3,已經不再兼容IE6-7-8了,所以部分兼容問題都已經統一了,例如:事件綁定的接口,事件對象的獲取等等

事件對象具體有些什么方法屬性參照 http://www.itxueyuan.org/view/6340.html

 


jQuery為dom處理而生,那么處理兼容的手段自然是獨樹一幟了,所以jQuery對事件的對象的兼容問題單獨抽象出一個類,用來重寫這個事件對象

jQuery 利用 jQuery.event.fix() 來解決跨瀏覽器的兼容性問題,統一接口。

除該核心方法外,統一接口還依賴於 (jQuery.event) props、 fixHooks、keyHooks、mouseHooks 等數據模塊。

props 存儲了原生事件對象 event 的通用屬性

keyHook.props 存儲鍵盤事件的特有屬性

mouseHooks.props 存儲鼠標事件的特有屬性。

keyHooks.filter 和 mouseHooks.filter 兩個方法分別用於修改鍵盤和鼠標事件的屬性兼容性問題,用於統一接口。

比如 event.which 通過 event.charCode 或 event.keyCode 或 event.button 來標准化。

最后 fixHooks 對象用於緩存不同事件所屬的事件類別,比如

fixHooks['click'] === jQuery.event.mouseHooks;

fixHooks['keydown'] === jQuery.event.keyHooks;

fixHooks['focusin'] === {};

 

從源碼處獲取對事件對象的操作,調用jQuery.Event重寫事件對象

// 將瀏覽器原生Event的屬性賦值到新創建的jQuery.Event對象中去
            event = new jQuery.Event( originalEvent );

event就是對原生事件對象的一個重寫了,為什么要這樣,JQuery要增加自己的處理機制唄,這樣更靈活,而且還可以傳遞data數據,也就是用戶自定義的數據

 

先看看源碼,如何處理事件對象兼容?

 

jQuery.Event構造函數

jQuery.Event = function( src, props ) {
    if ( src && src.type ) {
        this.originalEvent = src;
        this.type = src.type;
        this.isDefaultPrevented = ( src.defaultPrevented ||
            src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;
    } else {
        this.type = src;
    }
    if ( props ) {jQuery.extend( this, props );}
    this.timeStamp = src && src.timeStamp || jQuery.now();
    this[ jQuery.expando ] = true;
};

方法

jQuery.Event.prototype = {
    isDefaultPrevented: returnFalse,
    isPropagationStopped: returnFalse,   
    isImmediatePropagationStopped: returnFalse,    
    preventDefault: function() {
        var e = this.originalEvent;
        this.isDefaultPrevented = returnTrue;
        if ( e && e.preventDefault ) {e.preventDefault();}
    },
    stopPropagation: function() {
        var e = this.originalEvent;
        this.isPropagationStopped = returnTrue;
        if ( e && e.stopPropagation ) {e.stopPropagation(); }
    },
    stopImmediatePropagation: function() {
        this.isImmediatePropagationStopped = returnTrue;
        this.stopPropagation();
    }
};

大體過目下,有個大概的輪轂,后面用了在具體分析

構造出來的新對象

image

 

看圖,通過jQuery.Event構造器,僅僅只有一些定義的屬性與方法,但是原生的事件對象的屬性是不是丟了?

所以還需要把原生的的屬性給混入到這個新對象上

那么此時帶來一個問題,不同事件會產生了不同的事件對象,擁有不同的屬性,所以還的有一套適配的機制,根據不同的觸發點去適配需要混入的屬性名

 

擴展通過jQuery.Event構造出的新事件對象屬性

 

// 擴展事件屬性
this.fixHooks[ type ] = fixHook =
    rmouseEvent.test( type ) ? this.mouseHooks :
        rkeyEvent.test( type ) ? this.keyHooks :
        {};

 

有一些屬性是共用的,都存在,所以單獨拿出來就好了

props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),

 

然后把私有的與公共的拼接一下

copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;

 

然后混入到這個新的對象上

jQuery自己寫了一個基於native event的Event對象,並且把copy數組中對應的屬性從native event中復制到自己的Event對象中

while ( i-- ) {
    prop = copy[ i ];
    event[ prop ] = originalEvent[ prop ];
}

 

jQuery糾正了event.target對象

jQuery官方給的解釋是,Cordova沒有target對象

if ( !event.target ) {
                event.target = document;
            }

碰巧本人做的正是cordova項目

image

deviceready這個是設備准備就緒的事件,沒有target

 

在最后jQuery還不忘放一個鈎子,調用fixHook.fitler方法用以糾正一些特定的event屬性

例如mouse event中的pageX,pageY,keyboard event中的which

進一步修正事件對象屬性的兼容問題

fixHook.filter? fixHook.filter( event, originalEvent ) : event

fixHook就是在上一章,預處理的時候用到的,分解type存進去的,針對這個特性的單獨處理

最后返回這個“全新的”Event對象

 


事件對象默認方法的重寫

可見通過jQuery.Event構造出來的新的事件對象,就是對原生事件對象的一個加強版

重寫了preventDefault,stopPropagation,stopImmediatePropagation等接口由於這些方法經常會被調用中,所以這里分析一下

取消特定事件的默認行為

preventDefault: function() {
    var e = this.originalEvent;

    this.isDefaultPrevented = returnTrue;

    if ( e && e.preventDefault ) {
        e.preventDefault();
    }
},

重寫了preventDefault方法,但是現實上其實還是調用瀏覽器提供的e.preventDefault方法的,唯一的處理就是增加了一個

狀態機用來記錄,當前是否調用過這個方法

 this.isDefaultPrevented = returnTrue

同樣的stopPropagation,stopImmediatePropagation都增加了 this.isPropagationStopped與 this.isImmediatePropagationStopped,

 

所以最后構造出來的新對象,既有原生的屬性又多了很多自定義的屬性方法~~ 這樣jQuery可以用來玩花樣了。。。

image

 

總的來說jQuery.event.fix干的事情:

  • 將原生的事件對象 event 修正為一個新的可寫event 對象,並對該 event 的屬性以及方法統一接口
  • 該方法在內部調用了 jQuery.Event(event) 構造函數

 

jQuery對事件體系的修正不止是做了屬性兼容,重寫了事件的方法,還增加狀態機,那么這樣的處理有什么作用?

 


需要處理的的問題二:數據緩存

jQuery.cache 實現注冊事件處理程序的存儲,實際上綁定在 DOM元素上的事件處理程序只有一個,即 jQuery.cache[elem[expando]].handle 中存儲的函數,

所以只要在elem中取出當對應的prop編號去緩存中找到相對應的的事件句柄就行

這個簡單了,數據緩存本來就提供接口

handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [],

事件句柄拿到了,是不是立刻執行呢?當然不可以,委托還沒處理呢?

 


需要處理的的問題三:區分事件類型,組成事件隊列

事件的核心的處理來了,委托的重點

如何把回調句柄定位到當前的委托元素上面,如果有多個元素上綁定事件回調要如何處理

做這個操作之前,根據冒泡的原理,我們是不是應該把每一個節點層次的事件給規划出來,每個層次的依賴關系?

所以jQuery引入了jQuery.event.handlers用來區分普通事件與委托事件,形成一個有隊列關系的組裝事件處理包{elem, handlerObjs}的隊列

在最開始引入add方法中增加delegateCount用來記錄是否委托數,通過傳入的selector判斷,此刻就能派上用場了

先判斷下是否要處理委托,找到委托的句柄

根據之前的測試demo,

在元素DIV下面嵌套了P,然后P內嵌套了A

image

此刻就要進入關鍵點了

分二種情況處理

第一種自然是沒有委托,直接綁定的事件

body.on('click',function(){
        alert('灰')
    })

因為selector不存在所以delegateCount === 0,

所以委托處理的判斷不成立

if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {

此時直接組裝下返回elem與對應的handlers方法了

return handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });

image

 


第二種就是委托處理

我們取出當然綁定事件節點上的handlers,這個是在預分析的時候做的匹配關系,具體請看上一章

得到的處理關系

事件委托

從圖我們可以得出

1 元素本身有事件

2 元素又要處理委托事件

那么事件的執行就需要有個先后,jQuery要如何排序呢?

依賴委托節點在DOM樹的深度安排優先級,委托的DOM節點層次越深,其執行優先級越高

委托的事件處理程序相對於直接綁定的事件處理程序在隊列的更前面,委托層次越深,該事件處理程序則越靠前。

 

源碼的處理

if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {

    for ( ; cur !== this; cur = cur.parentNode || this ) {

        // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
        if ( cur.disabled !== true || event.type !== "click" ) {
            matches = [];
            for ( i = 0; i < delegateCount; i++ ) {
                handleObj = handlers[ i ];

                // Don't conflict with Object.prototype properties (#13203)
                sel = handleObj.selector + " ";

                if ( matches[ sel ] === undefined ) {
                    matches[ sel ] = handleObj.needsContext ?
                        jQuery( sel, this ).index( cur ) >= 0 :
                        jQuery.find( sel, this, null, [ cur ] ).length;
                }
                if ( matches[ sel ] ) {
                    matches.push( handleObj );
                }
            }
            if ( matches.length ) {
                handlerQueue.push({ elem: cur, handlers: matches });
            }
        }
    }
}

還有幾個判斷條件

如果有delegateCount,代表該事件是delegate類型的綁定

找出所有delegate的處理函數列隊

火狐瀏覽器右鍵或者中鍵點擊時,會錯誤地冒泡到document的click事件,並且stopPropagation也無效

if ( delegateCount && event.target.nodeType && (!event.button || event.type !== "click") ) {

 

在當前元素的父輩或者祖先輩有可能存在着事件綁定,根據冒泡的特性,我們的依次從當前節點往上遍歷一直到綁定事件的節點,取出每個綁定事件的節點對應的事件處理器

for ( ; cur !== this; cur = cur.parentNode || this ) {
      //遍歷節點
}

這里就有個cur === this 通過這個判斷來處理是否為正確的委托的

這里要注意各問題

假如elem.on('click','p',function(){}),我們在elem上點擊,那么在elem的作用范圍這個事件都會被觸發到,如果此刻用於的目標不在P元素,但是又滿足delegateCount存在

所以在cur===this,也就是目標對象就是elem了,那么判斷此點擊算無效點擊,但是注意事件在綁定的區域內都每次觸發都是會被執行的

 

遍歷的過程需要過濾一些節點,比如disabled 屬性規定應該禁用 input 元素,被禁用的 input 元素既不可用,也不可點擊

if ( cur.disabled !== true || event.type !== "click" ) {

 

此時開始處理委托過濾的關系了

sel = handleObj.selector + " ";

 

我們先確定下在當前的上下文中是否能找到這個selector元素

這里用到了sizzle選擇器去處理了

jQuery.find( sel, this, null, [ cur ] ).length;

如果能找到正確,是存在當然這個事件節點下面的元素,就是說這個節點是需要委托處理的

同樣的的組成一個handlerQueue

handlerQueue.push({ elem: cur, handlers: matches });

 

根據demo點擊a元素,會冒泡到P 最后到div,屬於handlerQueue就有a與p的處理器了

image

從這里我們可以看出delegate綁定的事件和普通綁定的事件是如何分開的。

對應一個元素,一個event.type的事件處理對象隊列在緩存里只有一個。

按照冒泡的執行順序,與元素的從內向外遞歸,以及handlers的排序,所以就處理了

所以就形成了事件隊列的委托在前,自身事件在后的順序,這樣也跟瀏覽器事件執行的順序一致了

區分delegate綁定和普通綁定的方法是:delegate綁定從隊列頭部推入,而普通綁定從尾部推入,通過記錄delegateCount來划分,delegate綁定和普通綁定。

 

總的來說jQuery.event.handlers干的事情:

將有序地返回當前事件所需執行的所有事件處理程序。

這里的事件處理程序既包括直接綁定在該元素上的事件處理程序,也包括利用冒泡機制委托在該元素的事件處理程序(委托機制依賴於 selector)。

在返回這些事件處理程序時,委托的事件處理程序相對於直接綁定的事件處理程序在隊列的更前面,委托層次越深,該事件處理程序則越靠前。

返回的結果是 [{elem: currentElem, handlers: handlerlist}, ...] 。

 


事件句柄緩存分析了

事件對象兼容分析了

委托關系分析了

 

在從頭看看事件執行的流程

綁定

elem.addEventListener( type, eventHandle, false );

事件句柄

eventHandle = elemData.handle = function( e ) {
    return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
        jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
        undefined;
};

這里其實用jQuery.event.dispatch.call就可以了,傳遞只是一個事件對象,然后this指向了這個事件元素elem

直接傳遞:jQuery.event.dispatch.call( eventHandle.elem, e)  這樣不更直接嗎?

 

call的性能在某些瀏覽器下要明顯比apply好,而其他瀏覽器中兩者差別不大

 


dispatch事件分發器源碼

dispatch: function( event ) {
    event 
=
 jQuery.event.fix( event );
    var i, j, ret, matched, handleObj,
        handlerQueue = [],
        args = core_slice.call( arguments ),
        handlers 
= ( data_priv.get( this, "events" ) || {} )[ event.type ] ||
 [],
        special = jQuery.event.special[ event.type ] || {};  
    args[0] = event;
    event.delegateTarget = this;
    if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
        return;
    }
    handlerQueue 
= jQuery.event.handlers.call( this
, event, handlers );
    i = 0;
    while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
        event.currentTarget = matched.elem;j = 0;
        while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
            if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {
                event.handleObj = handleObj;
                event.data = handleObj.data;
                ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
                    .apply( matched.elem, args );
                if ( ret !== undefined ) {
                    if ( (event.result = ret) === false ) {
                        event.preventDefault();
                        event.stopPropagation();
                    }
                }
            }
        }
    }
    if ( special.postDispatch ) {
        special.postDispatch.call( this, event );
    }
    return event.result;
},

dispatch事件分發器

可見依次處理了上面文章所以講的三個問題

  1. 事件句柄緩存讀取  data_priv.get
  2. 事件對象兼容       jQuery.event.fix
  3. 區分事件類型,組成事件隊列  jQuery.event.handlers

 

1,2與步都只做修飾性的處理,關鍵是handlers方法,我們從中取得了handlerQueue隊列

貼一下對handlerQueue事件隊列的處理方法

while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
    event.currentTarget = matched.elem;
    j = 0;
    while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
        if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {
            event.handleObj = handleObj; 
            event.data = handleObj.data;  
            ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
                .apply( matched.elem, args );

            if ( ret !== undefined ) {
                if ( (event.result = ret) === false ) {
                    event.preventDefault();
                    event.stopPropagation();
                }
            }
        }
    }
}

這個代碼就是針對handlerQueue的篩選了

1 最開始就分析的事件的執行順序,所以handlerQueue完全是按照事件的順序排列的,委托在前,本身的事件在后面

2 產生的事件對象其實只有一份,通過jQuery.Event構造出來的event

  在遍歷handlerQueue的時候修改了

  事件是綁定在父節點上的,所以此時的目標節點要通過替換,還有相對應的傳遞的數據,與處理句柄

  event.currentTarget = matched.elem;

  event.handleObj = handleObj;

  event.data = handleObj.data;

3 執行事件句柄

   ret = ((jQuery.event.special[handleObj.origType] || {}).handle || handleObj.handler).apply(matched.elem, args);  

4 如果有返回值 比如return false 

  系統就調用

  event.preventDefault();
  event.stopPropagation();

 


根據上面的分析我們就能很好的分析出on的執行流程了

在p1上綁定了自身事件,同事綁定了委托事件到li a p上都觸發,然后都調用同一個回調處理

var p1 = $('#p1')

    p1.on('click',function(){
        console.log('灰')
    })

    p1.on('click','li,a,p',function(e){
       console.log(e)
    })

處理的流程:

  1. 同一節點事件需要綁2次,各處理各的流程,寫入數據緩存elemData
  2. 這里要注意個問題,同一個節點上綁定多個事件,這個是在jQuery初始化綁定階段就優化掉的了,所以觸發時只會執行一次回調指令
  3. 觸發節點的時候,先包裝兼容事件對象,然后取出對應的elemData
  4. 遍歷綁定事件節點上的delegateCount數,分組事件
  5. delegate綁定從隊列頭部推入,而普通綁定從尾部推入,形成處理的handlerQueue
  6. 遍歷handlerQueue隊列,根據判斷是否isPropagationStopped,isImmediatePropagationStopped來處理對應是否執行
  7. 如果reuturn false則默認調用 event.preventDefault(); event.stopPropagation();

 


使用jQuery處理委托的優勢?

從以上的分析我們不難看,jQuery對於事件的處理是極其復雜的

那么jQuery 事件委托機制相對於瀏覽器默認的委托事件機制而言,有什么優勢?

不難發現其優勢在於委托的事件處理程序在執行時,其內部的 this 指向發出委托的元素(即滿足 selector 的元素),而不是被委托的元素,

記得吧

ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
                            .apply( matched.elem, args );

jQuery 在內部認為該事件處理程序還是綁定在那個發出委托的元素上,因此,如果開發人員在這個事件程序中中斷了事件擴散—— stopPropagation,那么后面的事件將不能執行。

 

當然還要涉及自定義事件,事件模擬,trigger與事件銷毀,在慢慢寫吧。。

 

文字挺多,個人見解不免有誤,歡迎大家指出~~ 

如果覺得還可以,就順手推薦下吧~


免責聲明!

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



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