jQuery源码解析之on事件绑定

Posted Youngly

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jQuery源码解析之on事件绑定相关的知识,希望对你有一定的参考价值。

本文采用的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:[0:{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;
    },
 
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]

  },

 

当有多个事件同时委托给同一个对象时,如document对象,每个事件绑定时的参数将存储在同一个document[expando]的value中,如下图中的click对象

 

 事件触发时,将获取到内存中所有的事件参数 ,进行选择器比对,然后执行对应的回调函数。

总结

on事件处理函数,主要是通过jquery中的一个内部对象dataPriv(如下图)来实现事件绑定时和事件触发时的数据流通,当多个子对象事件委托给同一个父级对象Root时,程序将根据事先存储在内存中的数据进行选择器匹配,然后执行对应的回调函数,接着事件会继续冒泡传给当前对象的父级,若父级也绑定了相同类型的事件,则触发父级绑定的回调函数,以此类推...直到最上层Root为止(确切地说,在一次子级点击事件触发时,子级和父级的回调函数依次按冒泡顺序执行;事件只触发了一次,而不是多次)。

 

 

 以上为研究jquery源码时对on事件绑定的一些总结,平常只会使用而没有研究过底层是如何实现的,写样例跟踪代码的过程中发现了不少以前没搞懂的细节,在此记录一下。

 

以上是关于jQuery源码解析之on事件绑定的主要内容,如果未能解决你的问题,请参考以下文章

浅谈jquery之on()绑定事件和off()解除绑定事件

petite-vue源码剖析-事件绑定`v-on`的工作原理

jQuery挖源码——事件绑定

jQuery 2.0.3 源码分析 事件绑定 - bind/live/delegate/on

jQuery之on

jQuery事件绑定.on()简要概述及应用