您的位置:首页 > Web前端 > JQuery

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

2015-10-14 17:00 696 查看
提前阅读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属性,也就是说他具有了该事件所有的信息。举例:

<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数组一看,哇

,这个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指定的,表示代理对象

<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是代理对象,是不变的!

我们看看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!target表示事件的真实触发处,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部分

<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学习请参考
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: