um..最近太忙了,这篇博客早在几个月前就想写来着,现在终于抽出时间来,另外应该是jQuery库的最后一篇分析了(也说不定哪天再写下jQuery的DOM选择器引擎)。
anyway,切入主题:
method shortcut:
// Return jQuery for attributes-only inclusion
jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
"change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {
// Handle event binding
jQuery.fn[ name ] = function( data, fn ) {
return arguments.length > 0 ?
this.on( name, null, data, fn ) :
this.trigger( name );
};
});
遍历数组添加快捷方法是jQuery常用的一种方式,根据参数长度来判断是绑定事件还是触发事件。先看事件绑定:
on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
var type, origFn;
if ( typeof types === "object" ) {
if ( typeof selector !== "string" ) {
data = data || selector;
selector = undefined;
}
for ( type in types ) {
this.on( type, selector, data, types[ type ], one );
}
return this;
}
if ( data == null && fn == null ) {
fn = selector;
data = selector = undefined;
} else if ( fn == null ) {
if ( typeof selector === "string" ) {
fn = data;
data = undefined;
} else {
fn = data;
data = selector;
selector = undefined;
}
}
if ( fn === false ) {
fn = returnFalse;
} else if ( !fn ) {
return this;
}
if ( one === 1 ) {
origFn = fn;
fn = function( event ) {
jQuery().off( event );
return origFn.apply( this, arguments );
};
fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
}
return this.each( function() {
jQuery.event.add( this, types, fn, data, selector );
});
}
on方法内部调用的实际是jQuery.event.add,on方法提供的只是多种方式的接口调用方式。
插嘴一句:jQuery处理参数大多是扁平化、统一化的方式,因为很多参数的可选的,所以会有很多交换参数位置的代码逻辑出现。
jQuery.event.add:
add: function( elem, types, handler, data, selector ) {
var tmp, events, t, handleObjIn,
special, eventHandle, handleObj,
handlers, type, namespaces, origType,
elemData = jQuery._data( elem );
if ( !elemData ) {
return;
}
if ( handler.handler ) {
handleObjIn = handler;
handler = handleObjIn.handler;
selector = handleObjIn.selector;
}
if ( !handler.guid ) {
handler.guid = jQuery.guid++;
}
if ( !(events = elemData.events) ) {
events = elemData.events = {};
}
if ( !(eventHandle = elemData.handle) ) {
eventHandle = elemData.handle = function( e ) {
return typeof jQuery !== strundefined && (!e || jQuery.event.triggered !== e.type) ?
jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
undefined;
};
eventHandle.elem = elem;
}
types = ( types || "" ).match( rnotwhite ) || [ "" ];
t = types.length;
while ( t-- ) {
tmp = rtypenamespace.exec( types[t] ) || [];
type = origType = tmp[1];
namespaces = ( tmp[2] || "" ).split( "." ).sort();
if ( !type ) {
continue;
}
special = jQuery.event.special[ type ] || {};
type = ( selector ? special.delegateType : special.bindType ) || type;
special = jQuery.event.special[ type ] || {};
handleObj = jQuery.extend({
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 );
if ( !(handlers = events[ type ]) ) {
handlers = events[ type ] = [];
handlers.delegateCount = 0;
if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
if ( elem.addEventListener ) {
elem.addEventListener( type, eventHandle, false ); } else if ( elem.attachEvent ) {
elem.attachEvent( "on" + type, eventHandle );
}
}
} if ( special.add ) {
special.add.call( elem, handleObj ); if ( !handleObj.handler.guid ) {
handleObj.handler.guid = handler.guid;
}
}
if ( selector ) {
handlers.splice( handlers.delegateCount++, 0, handleObj );
} else {
handlers.push( handleObj );
}
jQuery.event.global[ type ] = true;
}
elem = null;
}
暂时先不分析源码,先提一个问题:
Callback system(回调系统) VS event dispatch(事件分发)
首先看回调系统有什么问题:
回调系统的实现方式很简单,大多都是将一组回调函数放在一个数组中,然后遍历执行。
那么很严重的问题是:
当一个回调函数抛出异常时,剩余的回调函数将不会被执行。
try/catch ?
try/catch当然可以,不过你就看不到本应该抛出的异常了。
再看看事件分发的问题:
慢!对,就是慢。简单的往数组中push一个func比重复调用添加事件的API要快得多。
所以,解决方案是是什么:
个人的建议是使用try/catch结合setTimeout将异常抛出。(这个问题欢迎大家探讨)
setTimeout仍然存在一下问题:
1、性能问题,如果是一个图标的库,或者是mousemove这样的事件,那么setTimeout会很大程度上影响性能
2、debug,使用setTimeout导致无法debug了,丢失了调用栈。如果你的应用有自动上报错误的功能,那么这种错误上报上去会一点帮助也没有。
那么这里再提出两个方案:
1、简单的console.error()打印错误
2、使用一个数组来收集这些错误信息,然后统一处理或者上报。
回归正题:
jQuery的事件体系明显是用回调系统实现的,因为回调系统可以应用在任何对象上,随之带来的好处是你有了一个绑定/触发的事件系统。
add方法:
1、将回调函数push到handlers中
2、绑定handle处理函数到对象/元素上
3、根据selector判断是否是事件委托,使用handlers.delegateCount记录该对象的事件委托数量
4、对special事件做特殊处理
好处是同一事件只绑定一次,没有产生闭包变量引用(给handle处理函数添加了elem元素属性),不会导致内存泄露。
再看事件分发:
dispatch: function( event ) {
// Make a writable jQuery.Event from the native event object
event = jQuery.event.fix( event );
var i, ret, handleObj, matched, j,
handlerQueue = [],
args = slice.call( arguments ),
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
args[0] = event;
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;
}
// Determine handlers
handlerQueue = jQuery.event.handlers.call( this, event, handlers );
// Run delegates first; they may want to stop propagation beneath us
i = 0;
while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
event.currentTarget = matched.elem;
j = 0;
while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
// 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).
if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {
event.handleObj = handleObj;
event.data = handleObj.data;
ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
.apply( matched.elem, args );
if ( ret !== undefined ) {
if ( (event.result = ret) === false ) {
event.preventDefault();
event.stopPropagation();
}
}
}
}
}
// Call the postDispatch hook for the mapped type
if ( special.postDispatch ) {
special.postDispatch.call( this, event );
}
return event.result;
}
首先重写了event:event = jQuery.event.fix( event );
jQuery.event.fix方法在这里就不分析了(jQuery事件体系太庞大了,不可能所有的都做分析。嗯,我就是懒。)
然后确定了handlerQueue:handlerQueue = jQuery.event.handlers.call( this, event, handlers );(嗯,handlers方法也不分析。)
最后遍历执行回调列表(其中又包含了对special、事件传播、阻止默认事件的处理)。
再看事件触发:
jQuery的事件触发很复杂,很简单的Callbacks系统不一样,jQuery的事件体系,因为模拟了标准的DOM事件流程,所以一个事件还会进行冒泡。
所以trigger会在需要冒泡的时候(!onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ))进行获取到当前节点的事件冒泡路径。然后遍历路径检测改路径上是否注册了该事件的回调,如果有则执行,一直到冒泡结束或事件传播被stop时停下。最后判断该节点是否被阻止默认事件,如果不是则执行原生的DOM method。
事件的移除就不说了,原理就是从回调列表中删掉而已,唯一复杂的是控制委托数目和对特殊事件的处理。
事件作为DOM非常重要的一部分,怎么优化事件处理,提供优雅的API,适应更多的复杂情况,jQuery在这方面做得都很好,虽然jQuery渐渐退出前端的舞台,但其中的设计思路,Hack兼容等却不会消逝。
好了,差不多就是这些了。