jQuery-1.9.1源碼分析系列(十) 事件系統——主動觸發事件和模擬冒泡處理

  發現一個小點,先前沒有注意的node

stopPropagation: function() { var e = this.originalEvent; ... if ( e.stopPropagation ) { e.stopPropagation(); }

  jQuery重載stopPropagation函數調用的本地事件對象的stopPropagation函數阻止冒泡。也就是說,阻止冒泡的是當前節點,而不是事件源。react

 

  說到觸發事件,咱們第一反應是使用$(...).click()這種方式觸發click事件。這種方式毫無疑問簡潔明瞭,若是能使用這種方式推薦使用這種方式。可是若是是自定義事件呢?好比定義一個$(document).on("chuaClick","#middle",fn);這種狀況怎麼觸發事件?這就要用到$("#middle").trigger("chuaClick")了。瀏覽器

a.觸發事件低級API——jQuery.event.trigger


  trigger函數對全部類型事件的觸發提供了支持。這些事件主要分爲兩類:普通瀏覽器事件(包含帶有命名空間的事件如"click.chua")、自定義事件。由於要統一處理,因此函數內部實現沒有調用.click()這種方式來對普通瀏覽器事件作捷徑處理,而是統一流程。處理過程以下緩存

  1.獲取要觸發的事件(傳入的event多是事件類型而不是事件對象)app

event = event[ jQuery.expando ] ? event :new jQuery.Event( type, typeof event === "object" && event );

  2.修正瀏覽器事件(主要有修正事件源)和組合正確的事件處理參數data函數

if ( type.indexOf(".") >= 0 ) { //有命名空間的事件觸發; 先取出事件處理入口函數handle()使用的事件類型type
                namespaces = type.split("."); type = namespaces.shift(); namespaces.sort(); } ...// 調用者能夠傳遞jQuery.Event對象,普通對象,甚至是字符串
            event = event[ jQuery.expando ] ? event : new jQuery.Event( type, typeof event === "object" && event ); event.isTrigger = true; event.namespace = namespaces.join("."); event.namespace_re = event.namespace ?
            new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : null; // 重置result屬性,避免上次的結果殘留
            event.result = undefined; if ( !event.target ) { event.target = elem; } // 克隆傳參data並將event放在傳參data的前面,建立出事件處理入口函數的參數列表,建立後結果多是[event,data]
            data = data == null ? [ event ] : jQuery.makeArray( data, [ event ] );

  後面這段組合事件處理參數列表data在後面處理時調用post

if ( handle ) { handle.apply( cur, data ); }

  3.判斷是不是特殊節點對象的的特殊事件,是的話特殊處理this

  special = jQuery.event.special[ type ] || {};   if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {     return;   }

  這裏面須要特殊處理的事件比較少,這裏列一下spa

 special: { click.trigger: function(){ // checkbox, 觸發本地事件確保狀態正確if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { this.click(); return false; } }, focus.trigger: function() { // 觸發本地事件保證失焦/聚焦序列正確if ( this !== document.activeElement && this.focus ) { try { this.focus(); return false; } catch ( e ) { // Support: IE<9
                            // If we error on focus to hidden element (#1486, #12518),
                            // let .trigger() run the handlers
 } } }, blur.trigger: function() {if ( this === document.activeElement && this.blur ) { this.blur(); return false; } }
    }

  4.從事件源開始遍歷父節點直到Window對象,將通過的節點保存(保存到eventPath)下來備用設計

for ( ; cur; cur = cur.parentNode ) {   eventPath.push( cur );   tmp = cur; } // 將window也壓入eventPath(e.g., 不是普通對象也不是斷開鏈接的DOM)
if ( tmp === (elem.ownerDocument || document) ) {   eventPath.push( tmp.defaultView || tmp.parentWindow || window ); }

  5.循環先前保存的節點,訪問節點緩存,若是有對應的事件類型處理隊列則取出其綁定的事件(入口函數)進行調用。

        // jQuery綁定函數處理:判斷節點緩存中是否保存相應的事件處理函數,若是有則執行 handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); if ( handle ) { handle.apply( cur, data ); } // 本地綁定處理 handle = ontype && cur[ ontype ]; if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { event.preventDefault(); }

  6. 最後處理瀏覽器默認事件,好比submit標籤的提交表單處理。

// 若是沒有人阻止默認的處理,執行之
            if ( !onlyHandlers && !event.isDefaultPrevented() ) { ... }

   

  注意:普通事件加上命名空間仍然屬於普通事件,普通調用方式依然其做用。好比$(document).on('click.chua',"#id",fn1).on("click","#id",fn2);當點擊「#id」節點的時候fn1依然會被調用。觸發指定命名空間事件的惟一方式是trigger:$("#id").trigger("click.chua"),此時只會調用fn1。

  從第四、5個步驟能夠看到trigger的另一個巨大做用——模擬冒泡處理。後面會分析到

 

b. 事件特殊處理jQuery.event.special(主要有事件替代、模擬冒泡)詳解


  委託設計是基於事件可冒泡的。可是有些事件是不可冒泡的,有的事件在不一樣的瀏覽器上支持的冒泡狀況不一樣。還有不一樣的瀏覽器支持的事件類型也不盡相同。這些處理主要都被放在jQuery.event.special中。jQuery.event.special對象中保存着爲適配特定事件所需的變量和方法。

  具體有:

delegateType / bindType (用於事件類型的調整) setup (在某一種事件第一次綁定時調用) add (在事件綁定時調用) remove (在解除事件綁定時調用) teardown (在全部事件綁定都被解除時調用) trigger (在內部trigger事件的時候調用) noBubble _default handle (在實際觸發事件時調用) preDispatch (在實際觸發事件前調用) postDispatch (在實際觸發事件後調用)

  看一下模擬冒泡的函數simulate

simulate: function( type, elem, event, bubble ) { // 構建一個新的事件以區別先前綁定的事件.
            // 新構建的事件避免阻止冒泡, 但若是模擬事件能夠阻止默認操做的話,咱們作一樣的阻止默認操做。
            var e = jQuery.extend( new jQuery.Event(), event, { type: type, isSimulated: true, originalEvent: {} } ); if ( bubble ) { jQuery.event.trigger( e, null, elem ); } else { jQuery.event.dispatch.call( elem, e ); } if ( e.isDefaultPrevented() ) { event.preventDefault(); } }

  看到沒有,真正模擬冒泡函數是jQuery.event.trigger函數

 

special第一組

  這裏面涉及到冒泡處理的問題。

special: { load: { //阻止觸發image.load事件冒泡到window.load
        noBubble: true }, click: { //checkbox觸發時保證狀態正確
        trigger: function() {if (...) {this.click();return false;}} }, focus: { //觸發本當前節點blur/focus事件 確保隊列正確
        trigger: function() { if ( this !== document.activeElement && this.focus ) { try { this.focus(); return false; } catch ( e ) { // IE<9,若是咱們錯誤的讓隱藏的節點獲取焦點(#1486, #12518),
                    // 讓.trigger()運行處理器
 } } }, delegateType: "focusin" }, blur: { trigger: function() { if ( this === document.activeElement && this.blur ) { this.blur(); return false; } }, delegateType: "focusout" }, beforeunload: { postDispatch: function( event ) { //即便的returnValue等於undefined,Firefox仍然會顯示警告 
            if ( event.result !== undefined ) { event.originalEvent.returnValue = event.result; } } } }

  focus/blur原本是不冒泡的,可是咱們依然能夠經過$(document).on('focus ','#left',fn)來綁定,是怎麼作到的?咱們來看jQuery的處理

  第一步,將focus綁定的事件轉化爲focusin來綁定,focusin在W3C的標準中是冒泡的,除開火狐以外的瀏覽器也確實支持冒泡(火狐瀏覽器focusin/focusout支持冒泡的兼容後面會詳解)

type = ( selector ? special.delegateType : special.bindType ) || type;

  而後,根據新獲得的type類型(focusin)獲取新的special      

special = jQuery.event.special[ type ] || {};

  獲取的special結果爲

jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {   var attaches = 0,   handler = function( event ) {
    //模擬冒泡     jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ),
true );   };   jQuery.event.special[ fix ] = { setup: function() { if ( attaches++ === 0 ) { document.addEventListener( orig, handler, true ); } }, teardown: function() { if ( --attaches === 0 ) { document.removeEventListener( orig, handler, true ); } }   }; });

  再而後,就是綁定事件,綁定事件實際上就對focusin、focusout作了兼容處理,源碼中第一個判斷有special.setup.call(…)這段代碼,根據上面setup函數可見第一次進入的時候其實是經過setup函數中的document.addEventListener( orig, handler, true )綁定事件,注意:第一個參數是是orig,由於火狐不支持focusin/focusout因此jQuery使用focus/blur替代來監聽事件;注意第三個參數是true,表示在事件捕獲階段觸發事件

  咱們知道任何瀏覽器捕獲都是從外層到精確的節點的,全部的focusin事件都會被捕獲到,而後執行handler函數(裏面是jQuery.event.simulate函數,源碼略)。其餘事件綁定則進入if分支將事件直接綁定到elem上

if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {   if ( elem.addEventListener ) {     elem.addEventListener( type, eventHandle, false );   } else if ( elem.attachEvent ) {     elem.attachEvent( "on" + type, eventHandle );   } }

 

special第二組:mouseenter/mouseleave

//使用mouseover/out和事件時機檢測建立mouseenter/leave事件
jQuery.each({ mouseenter: "mouseover", mouseleave: "mouseout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { delegateType: fix, bindType: fix, handle: function( event ) { var ret, target = this, related = event.relatedTarget, handleObj = event.handleObj; //對於mousenter/leave,當related在target外面的時候才調用handler
                //參考: 當鼠標離開/進入瀏覽器窗口的時候是沒有relatedTarget的
                if ( !related || (related !== target && !jQuery.contains( target, related )) ) { event.type = handleObj.origType; ret = handleObj.handler.apply( this, arguments ); event.type = fix; } return ret; } }; });

  須要注意的是隻有在鼠標指針穿過被選元素時,纔會觸發 mouseenter 事件。對應mouseleave這樣的話,mouseenter子元素不會反覆觸發事件,不然在IE中常常有閃爍狀況發生

  使用mouseover/out和事件時機檢測建立mouseenter/leave事件有個關鍵的判斷

if ( !related || (related !== target && !jQuery.contains( target, related )) )

  其中!jQuery.contains( target, related )表示related在target外面。咱們使用圖例來解釋

  咱們假設處理的是mouseenter事件,進入target

  鼠標從related到target,很明顯related在target外面,因此當鼠標移動到target的時候知足條件,調用處理。

   

  如今反過來,很明顯related在target裏面,那麼鼠標以前就處於mouseenter狀態(意味着以前就進行了mouseenter處理器處理),避免重複調用固然是不進行任何處理直接返回了。

  

  咱們假設處理的是mouseleave事件,離開target

  鼠標從target到related,很明顯related在target裏面,因此當鼠標移動到related的時候依然麼有離開target,不作處理。

  

  鼠標從target到related,很明顯related在target外面,因此當鼠標移動到related的時候已經離開了target的範圍,作處理。

  

 

special第三組:submit和change

   

主要是ie下submit不能冒泡的處理

  jQuery.event.special.submit主要有一下幾個特徵

  setup

  postDispatch

  teardown

  根據添加事件的代碼可知添加事件的時候若是符合條件則會調用setup來添加事件

if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false )

  jQuery在ie下模擬submit事件以click和keypress替代,只不過是添加了命名空間來區別和普通click和keypress事件。

setup: function() {   ...   jQuery.event.add( this, "click._submit keypress._submit", function( e ) {     var elem = e.target,     form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined;       if ( form && !jQuery._data( form, "submitBubbles" ) ) {         jQuery.event.add( form, "submit._submit", function( event ) {           event._submit_bubble = true;         });         jQuery._data( form, "submitBubbles", true );       }   }); },

  在事件調用過程當中(dispatch)會調用postDispatch來處理

if ( special.postDispatch ) { special.postDispatch.call( this, event ); }

  postDispatch中調用simulate完成事件處理

postDispatch: function( event ) {   // If form was submitted by the user, bubble the event up the tree
  if ( event._submit_bubble ) {     delete event._submit_bubble;     if ( this.parentNode && !event.isTrigger ) {       jQuery.event.simulate( "submit", this.parentNode, event, true );     }   } },

  teardown用在刪除事件綁定中

 

  ie下change事件的處理和submit相似,事件使用beforeactivate替代來監聽,處理函數變成了handle,在事件分發(dispatch)中執行代碼

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

   主要源碼以下

jQuery.event.special.change = { setup: function() { //rformElems = /^(?:input|select|textarea)$/i
                if ( rformElems.test( this.nodeName ) ) { // IE不會在check/radio失焦前觸發change事件; 在屬性更改後觸發它的click事件
                    // 在special.change.handle中會吞掉失焦觸發的change事件.
                    // 這裏任然會在check/radio失焦後觸發onchange事件.
                    if ( this.type === "checkbox" || this.type === "radio" ) { jQuery.event.add( this, "propertychange._change", function( event ) { if ( event.originalEvent.propertyName === "checked" ) { this._just_changed = true; } }); jQuery.event.add( this, "click._change", function( event ) { if ( this._just_changed && !event.isTrigger ) { this._just_changed = false; } // Allow triggered, simulated change events (#11500)
                            jQuery.event.simulate( "change", this, event, true ); }); } return false; } // 事件代理; 懶惰模式爲後代input節點添加change事件處理
                jQuery.event.add( this, "beforeactivate._change", function( e ) { var elem = e.target; if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { jQuery.event.add( elem, "change._change", function( event ) { if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { jQuery.event.simulate( "change", this.parentNode, event, true ); } }); jQuery._data( elem, "changeBubbles", true ); } }); }, handle: function( event ) { var elem = event.target; // 吞掉本地單選框和複選框的change事件,咱們在上面已經出發了事件
                if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { return event.handleObj.handler.apply( this, arguments ); } },
    }

 

  OK,到此,事件系統也告一個段落了,謝謝你們多多支持。

  

  若是以爲本文不錯,請點擊右下方【推薦】!

相關文章
相關標籤/搜索