提前阅读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原生的事件绑定的内容!
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的引用!
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方法, 上下文为该通用函数的所有者,参数就是调用时候传入的参数!
解答:最重要的就是实例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!
解答:
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属性的!
ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ).apply( matched.elem, args );通过这里你就知道了,我们自己的函数中上下文就是当前事件流所在的DOM元素,而参数只有event对象,但是该event对象具有currentTarget,handleObj以及data属性,也就是说他具有了该事件所有的信息。举例:
<div id="parent"> <div id="child1" style="width:50px;height:50px;border:1px solid red;">child1</div> <div id="child2" style="width:50px;height:50px;border:1px solid red;margin-top:10px;">child2</div> </div>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数组一看,哇
解答:不是。我们看看源码中的这一部分就知道如何处理了
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事件不会被调用!
解答:delegateTarget是jQuery指定的,表示代理对象
<div id="parent"> <div id="child1" style="width:50px;height:50px;border:1px solid red;">child1</div> <div id="child2" style="width:50px;height:50px;border:1px solid red;margin-top:10px;">child2</div> </div>点击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是代理对象,是不变的!
$("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一般是事件被处理的地方
解答:可以
$("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部分
<div id="parent"> <div id="child" style="width:50px;height:50px;border:1px solid red;margin-top:10px;">child2</div> </div>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学习请参考