jQuery源码分析之jQuery.event.add和jQuery.event.dispatch十一问

提前阅读handles相关知识 正则表达式相关知识 jQuery.event.fix相关知识

问题1:jQuery.event.add有那些调用方式?

自定义事件click._submit keypress._submi:

	jQuery.event.add( this, "click._submit keypress._submit", function( e ) {
				// Node name check avoids a VML-related crash in IE (#9807)
				     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 );
				}
			});
自定义事件submit._submit:

jQuery.event.add( form, "submit._submit", function( event ) {
	event._submit_bubble = true;
			});
propertychange_change事件

jQuery.event.add( this, "propertychange._change", function( event ) {
						if ( event.originalEvent.propertyName === "checked" ) {
							this._just_changed = true;
						}
					});
click_change事件:

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 );
					});
beforeactive._change和change_changeg

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 );
				}
			});
问题2:我们添加的自定义事件步骤是什么?

第一步:获取该DOM本身用于保存数据的仓库,如果仓库获取失败(如传入的不是DOM或者JS对象那么直接返回,返回是undefined),那么无法添加事件直接返回

          elemData = jQuery._data( elem );
		// Don't attach events to noData or text/comment nodes (but allow plain objects)
		if ( !elemData ) {
			return;
		}
第二步:既然是添加事件,那么我们回调函数必须有一个独一无二的guid值

// Caller can pass in an object of custom data in lieu of the handler
		if ( handler.handler ) {
			handleObjIn = handler;
			handler = handleObjIn.handler;
			selector = handleObjIn.selector;
		}
         //为回调函数添加一个唯一的guid值
		// Make sure that the handler has a unique ID, used to find/remove it later
		if ( !handler.guid ) {
			handler.guid = jQuery.guid++;
		}
第三步:事件是添加在events域里面的,所以要组件events域

         //没有events域那么手动添加
		// Init the element's event structure and main handler, if this is the first
		if ( !(events = elemData.events) ) {
			events = elemData.events = {};
		}
第三步:上面的仓库里面还缺少一个执行所有回调事件的通用回调函数,那么我们添加。 但是添加了一个通用回调函数必须指定该通用回调函数由谁维护的!

           //如果没有handle通用回调函数则添加
		if ( !(eventHandle = elemData.handle) ) {//为DOM第二次添加回调时候不会多次添加通用回调函数,因为该DOM通用回调函数已经添加!
			eventHandle = elemData.handle = function( e ) {
				// Discard the second event of a jQuery.event.trigger() and
				// when an event is called after a page has unloaded
				return typeof jQuery !== strundefined && (!e || jQuery.event.triggered !== e.type) ?
					jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
					undefined;
			};
			// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
			eventHandle.elem = elem;//指定该通用回调函数所有者为当前DOM,如果下次再次为该DOM添加回调,那么不会再次添加回调函数
		}
第四步:获取我们需要添加的事件类型,多个类型之间用空格隔开

	types = ( types || "" ).match( rnotwhite ) || [ "" ];
		t = types.length;//可以添加多个类型
第五步:获取每一个事件类型的所有的参数,如类型名称,空间名称等,并构建一个独立的handleObj对象,同时把handleObj添加到特定类型事件的回调函数数组中,如果是click那么添加到click事件对应的回调数组中,如果是mouseover就添加到mouseover对应的回调数组中,每一个事件类型都有一个回调数组

                       while ( t-- ) {
			tmp = rtypenamespace.exec( types[t] ) || [];
			type = origType = tmp[1];
			namespaces = ( tmp[2] || "" ).split( "." ).sort();//排序,所以click.text1.text=click.text.text1!
			// There *must* be a type, no attaching namespace-only handlers
			if ( !type ) {
				continue;
			}
                      //看是否是特殊类型,如change_changeg,click_change等!
			// If event changes its type, use the special event handlers for the changed type
			special = jQuery.event.special[ type ] || {};
                        //如果含有selector表示当前DOM代理了满足selector的所有的同类的事件,那么获取delegateType!
			// If selector defined, determine special event api type, otherwise given type
			type = ( selector ? special.delegateType : special.bindType ) || type;
                      //如果代理了就是获取代理的类型
			// Update special based on newly reset type
			special = jQuery.event.special[ type ] || {};
			// handleObj is passed to all event handlers
			handleObj = jQuery.extend({//构建每一个事件独一无二的handleObj对象
				type: type,
				origType: origType,
				data: data,
				handler: handler,
				guid: handler.guid,
				selector: selector,
				needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
				namespace: namespaces.join(".")
			}, handleObjIn );
                  //如果带DOM对象的events域里面没有同类的事件,那么添加这个事件的一个回调数组!
			// Init the event handler queue if we're the first
			if ( !(handlers = events[ type ]) ) {
				handlers = events[ type ] = [];
				handlers.delegateCount = 0;
				//添加事件的时候,如果没有setup或者setup调用后返回是false,那么用addEventListener或者attachEvent来添加!
				// Only use addEventListener/attachEvent if the special events handler returns false
				if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
					// Bind the global event handler to the element
					if ( elem.addEventListener ) {
						elem.addEventListener( type, eventHandle, false );
					} else if ( elem.attachEvent ) {
						elem.attachEvent( "on" + type, eventHandle );
					}
				}
			}
                     //如果special有add方法那么调用add方法!
			if ( special.add ) {
				special.add.call( elem, handleObj );
				if ( !handleObj.handler.guid ) {
					handleObj.handler.guid = handler.guid;
				}
			}
                   //插入handleObj对象,这是插入特定的事件处理的回调数组中handlers
			// Add to the element's handler list, delegates in front
			if ( selector ) {
				handlers.splice( handlers.delegateCount++, 0, handleObj );
			} else {
				handlers.push( handleObj );
			}
                   //把global设置为true!
			// Keep track of which events have ever been used, for event optimization
			jQuery.event.global[ type ] = true;
		}
下面我们把第五步部分内容仔细分析一下:

              if ( !(handlers = events[ type ]) ) {
				handlers = events[ type ] = [];
				handlers.delegateCount = 0;
				//添加事件的时候,如果没有setup或者setup调用后返回是false,那么用addEventListener或者attachEvent来添加!
				// Only use addEventListener/attachEvent if the special events handler returns false
				if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
					// Bind the global event handler to the element
					if ( elem.addEventListener ) {
						elem.addEventListener( type, eventHandle, false );
					} else if ( elem.attachEvent ) {
						elem.attachEvent( "on" + type, eventHandle );
					}
				}
			}
note:从这里我们看到,每一个事件类型都对应的一个回调数组!但是为该DOM指定回调函数的时候首先是当作特殊类型来处理的,如果特殊类型无法处理,那么我们通过addEventListener或者attachEvent来绑定事件, 不过绑定的回调函数就是上面的通用函数,所以如果事件触发,那么执行的将会是通用的函数!所以说, 本质还是JS原生的事件绑定的内容!
如果这种特殊类型有add方法,那么我们还是需要调用add方法,同时如果通用函数没有guid我们把guid值设置为我们上面传入的回调函数的guid值

                    if ( special.add ) {
				special.add.call( elem, handleObj );
				if ( !handleObj.handler.guid ) {
					handleObj.handler.guid = handler.guid;//把通用函数的guid值设置为我们自己回调的guid值,该值在前面已经定义!
				}
			}
如果含有selector,那么表示当前对象是代理的,那么我们该DOM的delegateCount自增

       if ( selector ) {
		     handlers.splice( handlers.delegateCount++, 0, handleObj );
			} else {
				handlers.push( handleObj );
			}
                //把global设置为true!
			// Keep track of which events have ever been used, for event optimization
			jQuery.event.global[ type ] = true;
下面是时候看看他们的数据保存格式了,首先我们看看handleObj有那些内容,JS部分:

jQuery.event.add($("#father")[0],"qinliang",function()
	{
	  console.log("qinliang invoked!");
	});
通过 该图,你应该知道handleObj中的data就是绑定事件时候传入的数据,而handler就是我们绑定事件时候指定的回调函数(不是通用回调函数),selector用于判断是否进行了代理;type和origType都是事件类型;guid值就是我们为该函数分配的一个全局的标记。

问题3:handleObj的数据格式我知道了,那么这个handleObj是如何和我们绑定事件的DOM联系起来的?

jQuery.event.add($("#father")[0],"qinliang",function(e)//JS部分
	{
	  console.log("qinliang invoked!");
	});
	var expando=jQuery.expando;
	var key=$("#father")[0][expando];
	var data=jQuery.cache[key];
	console.log(data);
通过 博客,你会知道这是用钥匙开门的过程。我们来看看绑定在该DOM上面的数据是什么。通过 该图,你必须知道,我们为该DOM的仓库开辟了一个events域,用来处理各种事件类型, 每一种事件类型都是一个数组,数组中存放的是上面的handleObj对象。同时,你必须知道,该DOM中绑定的handle域是一个通用的回调函数,而不是我们自定义的回调函数,同时该通用回调函数的elem属性中持有div#father这个DOM元素的引用!那么,你可能会问,什么时候把这个通用回调函数绑定上去了?

                   if ( !(eventHandle = elemData.handle) ) {
			eventHandle = elemData.handle = function( e ) {
				// Discard the second event of a jQuery.event.trigger() and
				// when an event is called after a page has unloaded
				return typeof jQuery !== strundefined && (!e || jQuery.event.triggered !== e.type) ?
					jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
					undefined;
			};
			// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
			eventHandle.elem = elem;
		}
上面已经说过, 每一个元素的仓库都会添加一个handle域,用于处理所有的回调函数,而且这个回调函数只会在DOM上添加一次,而且这个回调函数本身也存在着对该DOM的引用!
问题4:那么我们添加了这个函数,具体执行过程是怎么样的呢?

if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
					// Bind the global event handler to the element
					if ( elem.addEventListener ) {
						elem.addEventListener( type, eventHandle, false );
					} else if ( elem.attachEvent ) {
						elem.attachEvent( "on" + type, eventHandle );
					}
				}
从这里我们可以看出,我们采用了JS通用的事件绑定方式来完成,那么当我们通过trigger(自定义事件可能不是通过用户行为来操控的)来触发事件的时候,这个DOM就能够接受到事件触发的行为,最终调用我们通用的回调函数eventHandle。

if ( !(eventHandle = elemData.handle) ) {
			eventHandle = elemData.handle = function( e ) {
				// Discard the second event of a jQuery.event.trigger() and
				// when an event is called after a page has unloaded
				return typeof jQuery !== strundefined && (!e || jQuery.event.triggered !== e.type) ?
					jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
					undefined;
			};
			// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
			eventHandle.elem = elem;
		}
在我们通用的函数中,会调用 jQuery.event.dispatch方法, 上下文为该通用函数的所有者,参数就是调用时候传入的参数!
问题5:源码中那些方法调用了jQuery.event.add方法?

解答:最重要的就是实例on方法

return this.each( function() {
			jQuery.event.add( this, types, fn, data, selector );
		});
所以,经过on方法,我们就可以把所有的元素都绑定事件处理函数了!见下例

$("#father").on("click","#child",{name:"qinliang"},function(e)
	{
	  console.log("1");//回调函数中参数是什么?
	});
	var expando=jQuery.expando;
	var key=$("#father")[0][expando];
	//注意:这里是进行了事件代理,所有事件回调都在father元素上面,而不在child上!
	var data=jQuery.cache[key];
	console.log(data);
我还想强调一下 该图, 事件处理函数click对应于一个回调函数数组,数组中存放的是handleObj对象;handleObj的对象中selector对应于代理的子元素的选择器,data就是我们传入的数据,handle就是我们传入的回调函数而不是通用的回调函数!因为这里father是代理了child的click事件,所以delegateCount就是1!

问题6:那么我们自己的函数真正调用的时候的上下文以及参数是什么?

解答:

var data = { id: 5, name: "张三" };  
     $("body").on("click.test.me1 mouseover.test.me2","#n5",data,function(e){alert(this.id+"我在测试add方法!"+e.data.name+e.delegateTarget.tagName+e.handleObj.selector)});  
 回调函数中this指向当前元素!,也就是currentTarget。apply( matched.elem, args )是在dispatch里面的源码,但是因为这里用的是apply而不是call,所以直接把args逐个封装到回调函数中。这个args只有一个参数args[0]=event所以这里回调函数中唯一一个参数就是event,同时可以通过event.data获取数据,通过delegateTarget获取代理对象BODY。通过handleObj可以获取通过add方法添加的事件的所有信息,也就是通过add方法里面添加的handleObj所有属性!同时可以通过currentTarget获取当前所在的元素! 
  总之一句话:在该回调函数中可以获取这个调用事件的所有的信息! 
  

问题7:我们回调jQuery.event.dispatch把,我们知道jQuery.event.add中绑定事件是通过addEventListener等常规方法的,所以当我们触发了这个事件的时候会传入event对象,同时上下文是该通用回调函数的所有者,也就是代理对象,那么该方法是如何调用函数的呢?

第一步:修正我们的event对象

event = jQuery.event.fix( event );
第二步:既然是调用事件,那么肯定是某一类事件了,那么我们要获取仓库里面的该类事件的所有handleObj对象了

                      args = slice.call( arguments ),
				//获取该DOM的events域下面保存的所有的事件的集合!
			handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [],
				//看事件是否是特殊事件
			special = jQuery.event.special[ event.type ] || {};
		// Use the fix-ed jQuery.Event rather than the (read-only) native event
		   //获取event对象!
		   args[0] = event;
		//代理对象就是上下文对象,如果不是代理this就是调用对象的DOM对象!
		event.delegateTarget = this;
		// Call the preDispatch hook for the mapped type, and let it bail if desired
		if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
			return;
		}
之所以jQuery.event.dispatch中的上下文是代理对象,这是因为我们在添加事件的时候决定的,也就是这个通用函数决定的

if ( !(eventHandle = elemData.handle) ) {
			eventHandle = elemData.handle = function( e ) {
				// Discard the second event of a jQuery.event.trigger() and
				// when an event is called after a page has unloaded
				return typeof jQuery !== strundefined && (!e || jQuery.event.triggered !== e.type) ?
					jQuery.event.dispatch.apply( eventHandle.elem, arguments ) ://上下文是代理对象!
					undefined;
			};
			// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
			eventHandle.elem = elem;
		}
第三步:既然代理对象的事件调用了,那么回调函数中时候需要知道当前事件流到那里了呢,看看事件流把

    handlerQueue = jQuery.event.handlers.call( this, event, handlers );//获取当前事件流在那里,包括当前所在的DOM等,以及当前DOM应该回调的handleObj集合!
第四步:把 jQuery.event.handlers所有的返回事件流信息用于函数回调, 不过这里不再回调通用函数,而是自己定义的回调函数!

    while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
			event.currentTarget = matched.elem;//当前事件流到那里了
			j = 0;
			while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
				//遍历当前DOM的满足的handleObj,以便回调!
				// Triggered event must either 1) have no namespace, or
				// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
		
			//如果event对象没有namespace_re属性,或者handleObj的namespace满足namespace_re正则表达式!
				if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {
					event.handleObj = handleObj;
					event.data = handleObj.data;
                    //经过上两步,该event对象具有了handleObj和data属性,handleObj具有该事件的所有的信息,本来data也有
					//但是这里把data也单独封装到event上
					ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
							.apply( matched.elem, args );
                     //真正开始执行我们自己的函数了(handleObj.handler),而不再是通用函数了!上下文是当前事件流的所在的DOM元素!
					 //参数是event,但是该event具有data保存了数据,具有handleObj保存了该事件所有的信息!
					if ( ret !== undefined ) {
						if ( (event.result = ret) === false ) {
							event.preventDefault();
							event.stopPropagation();
						}
					}
				}
			}
我们仔细分析一下这里的代码:

这里是什么鬼?

if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) )
如果通过jQuery.event.trigger来调用,那么event对象会具有namespace_re属性,因为通过 jQuery.event.trigger来调用的时候可以指定命名空间,click.text.text1和click.text1.text是相同的,因为内部是经过sort了。 所以,如果不是通过trigger来调用的话是没有namespace_re属性的!
问题8:我们自己指定的回调函数中的上下文以及参数是什么?

ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ).apply( matched.elem, args );
通过这里你就知道了,我们自己的函数中上下文就是当前事件流所在的DOM元素,而参数只有event对象,但是该event对象具有currentTarget,handleObj以及data属性,也就是说他具有了该事件所有的信息。举例:

child1
child2
JS部分

$("body").on("click","#child2",function(e){console.log(this);});//this指代child2元素!
$("body").on("click","#parent",function(e){console.log(this)});//this指代parent元素  
这时候你就会明白:虽然child2和parent的点击事件都是被body代理的,但是点击child2的时候,jQuery去查找body的click对应的handleObj数组一看,哇 ,这个child2满足我click中handleObj数组中的一个handleObj的选择器啊(通过jQuery.event.handlers发现的),于是他就触发了,给他一个上下文为自己,也就是child2元素,当然this也可以通过event.currentTarget来代替。 其它所有关于该事件的参数除了data可以通过e.data访问以外,都需要通过event.handleObj.x访问!
当事件流到了father的时候,jQuery一看,我靠,又满足body的关于click的handleObj数组的一个handleObj的选择器,于是他又触发了,这次的this是father元素,其它内容的访问和上面的方式一样! ( 之所以会查看body的click的handleObj数组还是由于事件冒泡了
问题9:子元素的事件被触发了以后,父元素也必须触发?

解答:不是。我们看看源码中的这一部分就知道如何处理了

if ( ret !== undefined ) {
			if ( (event.result = ret) === false ) {//只要返回函数返回false就能防止冒泡了!
				event.preventDefault();
				event.stopPropagation();
			}
}
他告诉我们,如果不想父元素触发,就必须阻止冒泡,只要return false就可以了
 $("body").on("click","#child2",function(e){console.log(this);return false;});//this指代child2元素!
   $("body").on("click","#parent",function(e){console.log(this)});//this指代parent元素  
这时候父元素的click事件不会被调用!
问题10:一般我们都有currentTarget,target,但是这里有了delegateTarget?

解答:delegateTarget是jQuery指定的,表示代理对象

child1
child2
点击child2元素:

$("body").on("click","#child2",function(e){
     console.log(e.target);//child2
     console.log(e.currentTarget);//child2
     console.log(e.delegateTarget);//body元素
  });
target就是触发事件的元素;currentTarget表示事件处理函数当前所在的元素,是一个动态的过程;delegateTarget是代理对象,是不变的!
我们看看currentTarget和target不同的情况

 $("body").on("click","#parent",function(e){
     console.log(e.target);//child2
     console.log(e.currentTarget);//parent
     console.log(e.delegateTarget);//body元素
  });
这时候currentTarget就是父元素了,而target还是子元素,delegateTarget还是body!t arget表示事件的真实触发处,currentTarget一般是事件被处理的地方
问题11:上面这种冒泡是否可以取消父元素的绑定的事件,而不仅仅是取消父元素的代理事件?

解答:可以

 $("body").on("click","#child2",function(e){
     console.log("child2");
  });
  $("body").click(function()
  {
    console.log("body");
  });
这时候点击child2两者都会被调用
  $("body").on("click","#child2",function(e){
     console.log("child2");
	 return false;
  });
  $("body").click(function()
  {
    console.log("body");
  });
这种情况下就可以防止事件冒泡到父元素上面,因为return false了!

下面给出一种jQuery自定义事件触发的方式:

HTML部分

child2
JS部分

jQuery.event.add($("#child")[0],"qinliang",function(e)//绑定qinliang事件
   {
     console.log("child");
   });
   var expando=jQuery.expando;
   var key=$("#child")[0][expando];
   var walhouse=jQuery.cache;
   var data=walhouse[key];
   console.log(data);//仓库里面的数据
   var event1=new jQuery.Event("qinliang")
   jQuery.event.dispatch.call($("#child")[0],event1);
这时候你会发现,我绑定的qinliang事件被触发了!

 如果要深入jQuery.event学习请参考  

你可能感兴趣的:(前端开发,jQuery源码,javascript)