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

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

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

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

 

本章分析的重點:java

經過addEventListener觸發事件後,回調句柄如何處理?node

具體來講就是,如何委派事件的,用到哪些機制,咱們若是用到項目上是否能借鑑?jquery

 

涉及的處理ajax

事件句柄的讀取與處理api

事件對象的兼容,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與事件銷燬,在慢慢寫吧。。

 

文字挺多,我的看法難免有誤,歡迎你們指出~~ 

若是以爲還能夠,就順手推薦下吧~

相關文章
相關標籤/搜索