jQuery事件
jQuery需要处理事件对象不同浏览器的兼容性。event 对象的属性和方法包含了当前事件的状态。在 W3C 规范中,event 对象是随事件处理函数传入的,Chrome、FireFox、Opera、Safari、IE9.0及其以上版本都支持这种方式;但是对于 IE8.0 及其以下版本,event 对象必须作为 window 对象的一个属性。event的某些属性只对特定的事件有意义。比如,fromElement 和 toElement 属性只对 onmouseover 和 onmouseout 事件有意义。
jQuery 利用 jQuery.event.fix()
来解决跨浏览器的兼容性问题,统一接口,除该核心方法外,统一接口还依赖于 (jQuery.event) props、 fixHooks、keyHooks、mouseHooks 等数据模块。
// 将浏览器原生Event的属性赋值到新创建的jQuery.Event对象中去
event = new jQuery.Event( originalEvent );
event就是对原生事件对象的一个重写了,jQuery要增加自己的处理机制,这样更灵活,而且还可以传递用户自定义的data数据。 来看一下构造函数
jQuery.Event = function( src, props ) {
if ( src && src.type ) {
this.originalEvent = src;
this.type = src.type;
this.isDefaultPrevented = ( src.defaultPrevented ||
src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;
} else {
this.type = src;
}
if ( props ) {jQuery.extend( this, props );}
this.timeStamp = src && src.timeStamp || jQuery.now();
this[ jQuery.expando ] = true;
};
//原型上的方法
jQuery.Event.prototype = {
isDefaultPrevented: returnFalse,
isPropagationStopped: returnFalse,
isImmediatePropagationStopped: returnFalse,
preventDefault: function() {
var e = this.originalEvent;
this.isDefaultPrevented = returnTrue;
if ( e && e.preventDefault ) {e.preventDefault();}
},
stopPropagation: function() {
var e = this.originalEvent;
this.isPropagationStopped = returnTrue;
if ( e && e.stopPropagation ) {e.stopPropagation(); }
},
stopImmediatePropagation: function() {
this.isImmediatePropagationStopped = returnTrue;
this.stopPropagation();
}
};
构造出来的对象是这样的
通过jQuery.Event构造器,仅仅只有一些定义的属性与方法,所以还需要把原生的的属性给拷贝到这个新对象上,因为原生对象的属性都是单层的,所以直接拷贝即可。
while ( i-- ) {
prop = copy[ i ];
event[ prop ] = originalEvent[ prop ];
}
事件对象默认方法的重写
jQuery.Event构造出来的新的事件对象,就是对原生事件对象的一个加强版,重写了preventDefault,stopPropagation,stopImmediatePropagation等接口由于这些方法经常会被调用中,所以这里分析一下
preventDefault: function() {
var e = this.originalEvent;
this.isDefaultPrevented = returnTrue;
if ( e && e.preventDefault ) {
e.preventDefault();
}
}
重写了preventDefault方法,但是实际上还是调用浏览器提供的e.preventDefault方法的,唯一的处理就是增加了一个状态机用来记录,当前是否调用过这个方法。
事件处理
jQuery.cache 实现注册事件处理程序的存储,实际上绑定在 DOM元素上的事件处理程序只有一个,即jQuery.cache[elem[expando]].handle 中存储的函数,所以只要在elem中取出当对应的prop编号去缓存中找到相对应的的事件句柄就行,数据缓存本来就提供接口
handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [],
事件句柄拿到了,我们还要处理委托。
遇到委托处理
dispatch: function( event ) {
//利用jQuery.event.fix()来解决跨浏览器的兼容性问题
event = jQuery.event.fix( event );
var handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [];
event.delegateTarget = this;
//核心,调用handler函数生成一个handler队列
handlerQueue = jQuery.event.handlers.call( this, event, handlers );
i = 0;
while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
event.currentTarget = matched.elem;
j = 0;
while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {
event.handleObj = handleObj;
event.data = handleObj.data;
ret = handleObj.handler.apply( matched.elem, args );
if ( ret !== undefined ) {
if ( (event.result = ret) === false ) {
event.preventDefault();
event.stopPropagation();
}
}
}
}
}
return event.result;
}
那么我们来看一下handlers函数
handlers: function( event, handlers ) {
var handlerQueue = [],
delegateCount = handlers.delegateCount,
cur = event.target;
//向上遍历DOM元素
for ( ; cur !== this; cur = cur.parentNode || this ) {
if ( cur.disabled !== true || event.type !== "click" ) {
matches = [];
for ( i = 0; i < delegateCount; i++ ) {
handleObj = handlers[ i ];
//获取handler的selector
sel = handleObj.selector + " ";
if ( matches[ sel ] === undefined ) {
matches[ sel ] = handleObj.needsContext ?
//查看通过selector筛选的元素是否包含cur
jQuery( sel, this ).index( cur ) >= 0 :
jQuery.find( sel, this, null, [ cur ] ).length;
}
//如果元素匹配成功,则把handleObj添加到matches数组。
if ( matches[ sel ] ) {
matches.push( handleObj );
}
}
//如果matches数组长度大于0,附加cur和matches到队列中
if ( matches.length ) {
handlerQueue.push({ elem: cur, handlers: matches });
}
}
}
if ( delegateCount < handlers.length ) {
//表示还有为委托事件函数,也要附加到队列中
handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
}
return handlerQueue;
}
cur = event.target(事件触发元素)和this(事件委托元素)。jQuery从cur通过parentNode 一层层往上遍历,通过selector匹配当前元素。
每一个cur元素都会遍历一次handlers。handlers的项是一个handleObj对象,包含selector属性。通过jQuery( sel, this ).index( cur )判断当前元素是否匹配,匹配成功就加到matches数组。
如何把回调句柄定位到当前的委托元素上面,如果有多个元素上绑定事件回调要如何处理?做这个操作之前,根据冒泡的原理,我们是不是应该把每一个节点层次的事件给规划出来,每个层次的依赖关系?在最开始引入add方法中增加delegateCount用来记录是否委托数,通过传入的selector判断,此刻就能派上用场了
第一种自然是没有委托,直接绑定的事件
body.on('click',function(){
//...
})
因为selector不存在所以delegateCount === 0,委托处理的判断不成立
if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {
此时直接组装下返回elem与对应的handlers方法了
return handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
第二种就是委托处理
当元素本身有事件,元素又要处理委托事件的时候怎么处理呢?依赖委托节点在DOM树的深度安排优先级,委托的DOM节点层次越深,其执行优先级越高。委托的事件处理程序相对于直接绑定的事件处理程序在队列的更前面,委托层次越深,该事件处理程序则越靠前。区分delegate绑定和普通绑定的方法是:delegate绑定从队列头部推入,而普通绑定从尾部推入,通过记录delegateCount来划分delegate绑定和普通绑定。
总的来说jQuery.event.handlers干的事情:
- 将有序地返回当前事件所需执行的所有事件处理程序。
- 这里的事件处理程序既包括直接绑定在该元素上的事件处理程序,也包括利用冒泡机制委托在该元素的事件处理程序(委托机制依赖于 selector)。
- 在返回这些事件处理程序时,委托的事件处理程序相对于直接绑定的事件处理程序在队列的更前面,委托层次越深,该事件处理程序则越靠前。
那么jQuery 事件委托机制相对于浏览器默认的委托事件机制而言,有什么优势?
一个优势在于委托的事件处理程序在执行时,其内部的 this 指向发出委托的元素(即满足 selector 的元素),而不是被委托的元素
ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
.apply( matched.elem, args );
当然还涉及自定义事件,事件模拟,trigger与事件销毁
自定义事件
当我们在对象A和对象B之间交互的时候,有这两种方式:
一是对象A直接调用对象B的某个方法,实现交互;直接方法调用本质上也是属于一种特殊的发送与接受消息,它把发送消息和接收消息合并为一个动作完成;方法调用方和被调用方被紧密耦合在一起;因为发送消息和接收消息是在一个动作内完成,所以无法做到消息的异步发送和接收;
二是对象A生成消息->将消息通知给一个事件消息处理器(Observable)->消息处理器通过同步或异步的方式将消息传递给接收者;这种方式是通过将消息发送和消息接收拆分为两个过程,通过一个中间者来控制消息是同步还是异步发送;在消息通信的灵活性方面比较有优势,但是也带来了一定的复杂度。这种方式在设计模式中类似于观察者模式,可以参考我另一篇博文js观察者模式。但是复杂度一般可以由框架封装,消息的发送方和接收方仍然可以做到比较简单。
jQuery的事件自定义事件还是通过on绑定的,然后再通过trigger来触发这个事件
/给element绑定hello事件
element.bind("hello",function(){
alert("hello world!");
});
//触发hello事件
element.trigger("hello");
trigger(type, data)方法有两个参数,第一个参数是要触发的事件类型,第二个单数是要传递给事件处理函数的附加数据,以数组形式传递。通常可以通过传递一个参数给回调函数来区别这次事件是代码触发的还是用户触发的。
自定义事件既可以冒泡,也可以被拦截。和on方法一样是模拟了事件对象
event = event[ jQuery.expando ] ?
event :
new jQuery.Event( type, typeof event === "object" && event );