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事件绑定的主要内容,如果未能解决你的问题,请参考以下文章
petite-vue源码剖析-事件绑定`v-on`的工作原理