1. 程式人生 > >jQuery源碼解析之on事件綁定

jQuery源碼解析之on事件綁定

lse select UNC 元素 註意 port button ron targe

本文采用的jQuery源碼為jquery-3.2.1.js

jquery的on方法用來在選定的元素上綁定一個或多個事件處理函數。

當參數selector存在時,通常會用來對已經存在的元素或將來即將添加到文檔中的元素做事件委托,表示當點擊document中的selector元素時,將觸發function回調函數。

 1 <div id="div" style="font-weight:800;font-size:24px;text-align:center;color:red;">
 2     <p id="paragraph">test events </p>
 3
</div> 4 5 <script src="../jquery-3.2.1.js"></script> 6 <script> 7 $(function(){ 8 $(document).on("click","#paragraph",function(){ 9 console.log(‘this is paragraph‘); 10 }); 11 $(document).on("click","#div",function(){ 12 console.log(‘this is div‘);
13 }); 14 $(document).on("click","#addDiv",function(){ 15 console.log(‘this is add div‘); 16 }); 17 18 setTimeout(function(){ 19 createDiv(); 20 },2000); 21 }); 22 23 function createDiv(){ 24 $("<div>").attr("id","addDiv").html("this is a div add after event binding").appendTo($("body"));
25 } 26 </script>

上面例子中三個事件都綁定在document對象上,當鼠標點擊最內層的#paragraph時,會看到瀏覽器打出的結果是

技術分享圖片

可以得知,綁定的事件是在冒泡階段觸發的。

查看源碼

 1 jQuery.fn.extend( {
 2 
 3     on: function( types, selector, data, fn ) {
 4         return on( this, types, selector, data, fn );
 5     },
 6 //.....
 7 
 8 //on方法對傳入的參數做了一些轉換
 9 function on( elem, types, selector, data, fn, one ) {
10     var origFn, type;
11 
12     // Types can be a map of types/handlers
13     if ( typeof types === "object" ) {
14 
15         // ( types-Object, selector, data )
16         if ( typeof selector !== "string" ) {
17 
18             // ( types-Object, data )
19             data = data || selector;
20             selector = undefined;
21         }
22         for ( type in types ) {
23             on( elem, type, selector, data, types[ type ], one );
24         }
25         return elem;
26     }
27 
28     if ( data == null && fn == null ) {
29 
30         // ( types, fn )
31         fn = selector;
32         data = selector = undefined;
33     } else if ( fn == null ) {
34         if ( typeof selector === "string" ) {
35 
36             // ( types, selector, fn )
37             fn = data;
38             data = undefined;
39         } else {
40 
41             // ( types, data, fn )
42             fn = data;
43             data = selector;
44             selector = undefined;
45         }
46     }
47     if ( fn === false ) {
48         fn = returnFalse;
49     } else if ( !fn ) {
50         return elem;
51     }
52 
53     if ( one === 1 ) {
54         origFn = fn;
55         fn = function( event ) {
56 
57             // Can use an empty set, since event contains the info
58             jQuery().off( event );
59             return origFn.apply( this, arguments );
60         };
61 
62         // Use same guid so caller can remove using origFn
63         fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
64     }
65 //最終的結果由jQuery.event.add方法實現
66     return elem.each( function() {
67         jQuery.event.add( this, types, fn, data, selector );
68     } );
69 }
  1 add: function( elem, types, handler, data, selector ) {
  2 
  3         var handleObjIn, eventHandle, tmp,
  4             events, t, handleObj,
  5             special, handlers, type, namespaces, origType,
  6             elemData = dataPriv.get( elem );//此處從內存中獲取document對象的數據,
    //第一次綁定時是沒有數據的,程序將執行cache方法,創建一個{}值作為document的值,並返回該值的引用。
    //若內存中已存在document的數據,則直接返回。
    //此時
elemData為document對象在內存中的數據的引用,下面將為它賦值

cache: function( owner ) {

    // Check if the owner object already has a cache
  var value = owner[ this.expando ];

    // If not, create one
  if ( !value ) {
    value = {};

// We can accept data for non-element nodes in modern browsers,
// but we should not, see #8335.
// Always return an empty object.
  if ( acceptData( owner ) ) {

    // If it is a node unlikely to be stringify-ed or looped over
    // use plain assignment
      if ( owner.nodeType ) {
      owner[ this.expando ] = value;

      // Otherwise secure it in a non-enumerable property
      // configurable must be true to allow the property to be
      // deleted when data is removed
      } else {
        Object.defineProperty( owner, this.expando, {
          value: value,
          configurable: true
        } );
      }
    }
  }

  return value;
},

get: function( owner, key ) {
    return key === undefined ?

          this.cache( owner ) :

          // Always use camelCase key (gh-2257)

        owner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ];

},

  7 
  8         // Don‘t attach events to noData or text/comment nodes (but allow plain objects)
  9         if ( !elemData ) {
 10             return;
 11         }
 12 
 13         // Caller can pass in an object of custom data in lieu of the handler
 14         if ( handler.handler ) {
 15             handleObjIn = handler;
 16             handler = handleObjIn.handler;
 17             selector = handleObjIn.selector;
 18         }
 19 
 20         // Ensure that invalid selectors throw exceptions at attach time
 21         // Evaluate against documentElement in case elem is a non-element node (e.g., document)
 22         if ( selector ) {
 23             jQuery.find.matchesSelector( documentElement, selector );
 24         }
 25 
 26         // Make sure that the handler has a unique ID, used to find/remove it later
 27         if ( !handler.guid ) {
 28             handler.guid = jQuery.guid++;
 29         }
 30 
 31         // Init the element‘s event structure and main handler, if this is the first
 32         if ( !( events = elemData.events ) ) {
 33             events = elemData.events = {};//為elemData添加events對象屬性,
 34         }
 35         if ( !( eventHandle = elemData.handle ) ) {
 36             eventHandle = elemData.handle = function( e ) {//事件觸發時,調用該函數;為elemData添加handle方法
 37 
 38                 // Discard the second event of a jQuery.event.trigger() and
 39                 // when an event is called after a page has unloaded
 40                 return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
 41                     jQuery.event.dispatch.apply( elem, arguments ) : undefined;
 42             };
 43         }
 44 
 45         // Handle multiple events separated by a space
 46         types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
 47         t = types.length;
 48         while ( t-- ) {
 49             tmp = rtypenamespace.exec( types[ t ] ) || [];
 50             type = origType = tmp[ 1 ];
 51             namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
 52 
 53             // There *must* be a type, no attaching namespace-only handlers
 54             if ( !type ) {
 55                 continue;
 56             }
 57 
 58             // If event changes its type, use the special event handlers for the changed type
 59             special = jQuery.event.special[ type ] || {}; 60 
 61             // If selector defined, determine special event api type, otherwise given type
 62             type = ( selector ? special.delegateType : special.bindType ) || type;
 63 
 64             // Update special based on newly reset type
 65             special = jQuery.event.special[ type ] || {};
 66 
 67             // handleObj is passed to all event handlers
 68             handleObj = jQuery.extend( {//將事件綁定時傳入的參數:事件類型、選擇器、回調函數等封裝入handleObj對象中
 69                 type: type,
 70                 origType: origType,
 71                 data: data,
 72                 handler: handler,
 73                 guid: handler.guid,
 74                 selector: selector,
 75                 needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
 76                 namespace: namespaces.join( "." )
 77             }, handleObjIn );
 78 
 79             // Init the event handler queue if we‘re the first
 80             if ( !( handlers = events[ type ] ) ) {
 81                 handlers = events[ type ] = [];//為elemData.events.click賦值為[],同時handlers指向該數組
 82                 handlers.delegateCount = 0;
 83 
 84                 // Only use addEventListener if the special events handler returns false
 85                 if ( !special.setup ||
 86                     special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
 87 
 88                     if ( elem.addEventListener ) {//綁定事件,註意事件是綁定到elem上的,即document對象上
 89                         elem.addEventListener( type, eventHandle );
 90                     }
 91                 }
 92             }
 93 
 94             if ( special.add ) {
 95                 special.add.call( elem, handleObj );
 96 
 97                 if ( !handleObj.handler.guid ) {
 98                     handleObj.handler.guid = handler.guid;
 99                 }
100             }
101 
102             // Add to the element‘s handler list, delegates in front
103             if ( selector ) {
104                 handlers.splice( handlers.delegateCount++, 0, handleObj );
/**將handleObj中的屬性插入到handlers中,即
document在內存中的數據={handle:f(),
    events:{click:[{type: "click", origType: "click", data: undefined, guid: 1, handler: ?(),selector:"#div",namespace:"",needContext:false}},
        delegateCount:1]}}**/
105 } else { 106 handlers.push( handleObj ); 107 } 108 109 // Keep track of which events have ever been used, for event optimization 110 jQuery.event.global[ type ] = true; 111 } 112 113 },//事件綁定完成

由事件綁定過程可以看出,事件觸發時執行eventHandle函數,而eventHanle最終執行事件派發:jQuery.event.dispatch.apply( elem, arguments )

 1 dispatch: function( nativeEvent ) {
 2 
 3         // Make a writable jQuery.Event from the native event object
 4         var event = jQuery.event.fix( nativeEvent );//將原生事件對象轉化為jquery的event
 5 
 6         var i, j, ret, matched, handleObj, handlerQueue,
 7             args = new Array( arguments.length ),
 8             handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [],//獲取事件綁定時document存儲的參數值
 9             special = jQuery.event.special[ event.type ] || {};
10 
11         // Use the fix-ed jQuery.Event rather than the (read-only) native event
12         args[ 0 ] = event;
13 
14         for ( i = 1; i < arguments.length; i++ ) {
15             args[ i ] = arguments[ i ];
16         }
17 
18         event.delegateTarget = this;
19 
20         // Call the preDispatch hook for the mapped type, and let it bail if desired
21         if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
22             return;
23         }
24 
25         // Determine handlers
26         handlerQueue = jQuery.event.handlers.call( this, event, handlers );//從document的所有事件參數中,篩選出當前點擊目標的參數對象
    handlers: function( event, handlers ) {
        var i, handleObj, sel, matchedHandlers, matchedSelectors,
            handlerQueue = [],
            delegateCount = handlers.delegateCount,//委派次數
            cur = event.target;//委派的目標,如#addDiv

        // Find delegate handlers
        if ( delegateCount &&

            // Support: IE <=9
            // Black-hole SVG <use> instance trees (trac-13180)
            cur.nodeType &&

            // Support: Firefox <=42
            // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861)
            // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click
            // Support: IE 11 only
            // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343)
            !( event.type === "click" && event.button >= 1 ) ) {

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

                // Don‘t check non-elements (#13208)
                // Don‘t process clicks on disabled elements (#6911, #8165, #11382, #11764)
                if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) {
                    matchedHandlers = [];
                    matchedSelectors = {};
                    for ( i = 0; i < delegateCount; i++ ) {//遍歷委派的事件參數數組,當selector=當前點擊對象cur時,將對應的參數對象放入handlerQueue中
    //註意:遍歷時對於selector指向的頁面元素,無論它是頁面加載時已經存在的元素,還是頁面加載完成後通過js後來生成的元素,當點擊該元素時,程序都能實現對其回調函數的調用。
    //這便是為什麽on可以對未來出現的元素進行事件綁定了。
                        handleObj = handlers[ i ];

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

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

        // Add the remaining (directly-bound) handlers
        cur = this;
        if ( delegateCount < handlers.length ) {
            handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } );
        }

        return handlerQueue;
    },
27 
28         // Run delegates first; they may want to stop propagation beneath us
29         i = 0;
30         while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {
31             event.currentTarget = matched.elem;
32 
33             j = 0;
34             while ( ( handleObj = matched.handlers[ j++ ] ) &&
35                 !event.isImmediatePropagationStopped() ) {//沒有調用event.stopImmediatePropagation() 方法,即沒有阻止事件傳播
36 
37                 // Triggered event must either 1) have no namespace, or 2) have namespace(s)
38                 // a subset or equal to those in the bound event (both can have no namespace).
39                 if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) {
40 
41                     event.handleObj = handleObj;
42                     event.data = handleObj.data;
43 
44                     ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||
45                         handleObj.handler ).apply( matched.elem, args );//執行回調函數matched.elem.func(args);
    //突然發現原來是目標elem執行的func回調函數,這也是為什麽回調函數中的$(this)會指向當前綁定的jquery對象了。
46 47 if ( ret !== undefined ) {//如果回調函數返回值為false時,則阻止事件的默認操作和後續的冒泡傳播 48 if ( ( event.result = ret ) === false ) { 49 event.preventDefault(); 50 event.stopPropagation(); 51 } 52 } 53 } 54 } 55 } 56 57 // Call the postDispatch hook for the mapped type 58 if ( special.postDispatch ) { 59 special.postDispatch.call( this, event ); 60 } 61 62 return event.result; 63 },

但程序是如何取到document在綁定事件時存儲在內存中的數據的呢?

可以看到,我們獲取內存中的數據時是通過dataPriv對象來獲取的,頁面加載時會自動創建dataPriv對象,裏面包含當前文檔中唯一的expando 擴展屬性。

 1 var dataPriv = new Data();
 2 //......
 3 
 4 function Data() {
 5     this.expando = jQuery.expando + Data.uid++;
 6 }
 7 Data.uid = 1;
 8 //......
 9 
10 jQuery.extend( {
11 
12     // Unique for each copy of jQuery on the page
13     expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
14 //......

當第一次為document對象創建內存對象時,內存對象關聯到expando屬性;當事件觸發時,通過dataPriv.get()方法獲取到document對應的expando的值對象。這樣就保證了事件前後的數據統一。

cache: function( owner ) {

    // Check if the owner object already has a cache
  var value = owner[ this.expando ];

    // If not, create one
  if ( !value ) {
    value = {};

// We can accept data for non-element nodes in modern browsers,
// but we should not, see #8335.
// Always return an empty object.
  if ( acceptData( owner ) ) {

    // If it is a node unlikely to be stringify-ed or looped over
    // use plain assignment
      if ( owner.nodeType ) {
      owner[ this.expando ] = value;//即document[jQuery3210080552146542722581]={}

      // Otherwise secure it in a non-enumerable property
      // configurable must be true to allow the property to be
      // deleted when data is removed
      } else {
        Object.defineProperty( owner, this.expando, {
          value: value,
          configurable: true
        } );
      }
    }
  }

  return value;
},

  get: function( owner, key ) {
    return key === undefined ?

          this.cache( owner ) :

          // Always use camelCase key (gh-2257)

        owner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ];

//即document[jQuery3210080552146542722581]={handle:f(),events:{
      click:[{type: "click", origType: "click", data: undefined, guid: 1, handler: ?(),selector:"#div",namespace:"",needContext:false}},
      delegateCount:1]}}

  },

 

當有多個事件同時委托給同一個對象時,如document對象,每個事件綁定時的參數將存儲在同一個document[expando]的value中,如下圖中的click對象

技術分享圖片

事件觸發時,將獲取到內存中所有的事件參數進行選擇器比對,然後執行對應的回調函數。

技術分享圖片

以上為研究jquery源碼時對on事件綁定的一些總結,平常只會使用而沒有研究過底層是如何實現的,寫樣例跟蹤代碼的過程中發現了不少以前沒搞懂的細節,在此記錄一下。

jQuery源碼解析之on事件綁定