A large refactor of the event handling logic. Data and namespace information is maintained in a separate object now, no longer on the event handler. Proxy functions are no longer needed, as a result. Additionally execution order of the handlers is maintained, fixing #4261, and the execution of handlers is maintained even while they're being removed. Live events will be refactored separately.

This commit is contained in:
jeresig 2010-02-04 00:20:52 -05:00
parent 8a4b2102ff
commit e7912805d6
2 changed files with 144 additions and 150 deletions

View file

@ -29,18 +29,6 @@ jQuery.event = {
handler.guid = jQuery.guid++; handler.guid = jQuery.guid++;
} }
// if data is passed, bind to handler
if ( data !== undefined ) {
// Create temporary function pointer to original handler
var fn = handler;
// Create unique handler function, wrapped around original handler
handler = jQuery.proxy( fn );
// Store data in unique handler
handler.data = data;
}
// Init the element's event structure // Init the element's event structure
var elemData = jQuery.data( elem ); var elemData = jQuery.data( elem );
@ -50,25 +38,22 @@ jQuery.event = {
return; return;
} }
var events = elemData.events || (elemData.events = {}), var events = elemData.events = elemData.events || {},
handle = elemData.handle, eventHandle; eventHandle = elemData.handle, eventHandle;
if ( !handle ) { if ( !eventHandle ) {
eventHandle = function() { elemData.handle = eventHandle = function() {
// Handle the second event of a trigger and when // Handle the second event of a trigger and when
// an event is called after a page has unloaded // an event is called after a page has unloaded
return typeof jQuery !== "undefined" && !jQuery.event.triggered ? return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
jQuery.event.handle.apply( eventHandle.elem, arguments ) : jQuery.event.handle.apply( eventHandle.elem, arguments ) :
undefined; undefined;
}; };
handle = elemData.handle = eventHandle;
} }
// Add elem as a property of the handle function // Add elem as a property of the handle function
// This is to prevent a memory leak with non-native // This is to prevent a memory leak with non-native events in IE.
// event in IE. eventHandle.elem = elem;
handle.elem = elem;
// Handle multiple events separated by a space // Handle multiple events separated by a space
// jQuery(...).bind("mouseover mouseout", fn); // jQuery(...).bind("mouseover mouseout", fn);
@ -77,32 +62,32 @@ jQuery.event = {
var type, i = 0, namespaces; var type, i = 0, namespaces;
while ( (type = types[ i++ ]) ) { while ( (type = types[ i++ ]) ) {
if ( i > 1 ) { var handleObj = {
handler = jQuery.proxy( handler ); handler: handler,
data: data,
if ( data !== undefined ) { namespace: "",
handler.data = data; guid: handler.guid
} };
}
// Namespaced event handlers // Namespaced event handlers
if ( type.indexOf(".") > -1 ) { if ( type.indexOf(".") > -1 ) {
namespaces = type.split("."); namespaces = type.split(".");
type = namespaces.shift(); type = namespaces.shift();
handler.type = namespaces.slice(0).sort().join("."); handleObj.namespace = namespaces.slice(0).sort().join(".");
} else { } else {
namespaces = []; namespaces = [];
handler.type = "";
} }
handleObj.type = type;
// Get the current list of functions bound to this event // Get the current list of functions bound to this event
var handlers = events[ type ], var handlers = events[ type ],
special = this.special[ type ] || {}; special = jQuery.event.special[ type ] || {};
// Init the event handler queue // Init the event handler queue
if ( !handlers ) { if ( !handlers ) {
handlers = events[ type ] = {}; handlers = events[ type ] = [];
// Check for a special event handler // Check for a special event handler
// Only use addEventListener/attachEvent if the special // Only use addEventListener/attachEvent if the special
@ -110,29 +95,23 @@ jQuery.event = {
if ( !special.setup || special.setup.call( elem, data, namespaces, handler ) === false ) { if ( !special.setup || special.setup.call( elem, data, namespaces, handler ) === false ) {
// Bind the global event handler to the element // Bind the global event handler to the element
if ( elem.addEventListener ) { if ( elem.addEventListener ) {
elem.addEventListener( type, handle, false ); elem.addEventListener( type, eventHandle, false );
} else if ( elem.attachEvent ) { } else if ( elem.attachEvent ) {
elem.attachEvent( "on" + type, handle ); elem.attachEvent( "on" + type, eventHandle );
} }
} }
} }
if ( special.add ) { if ( special.add ) {
var modifiedHandler = special.add.call( elem, handler, data, namespaces, handlers ); special.add.call( elem, handleObj );
if ( modifiedHandler && jQuery.isFunction( modifiedHandler ) ) {
modifiedHandler.guid = modifiedHandler.guid || handler.guid;
modifiedHandler.data = modifiedHandler.data || handler.data;
modifiedHandler.type = modifiedHandler.type || handler.type;
handler = modifiedHandler;
}
} }
// Add the function to the element's handler list // Add the function to the element's handler list
handlers[ handler.guid ] = handler; handlers.push( handleObj );
// Keep track of which events have been used, for global triggering // Keep track of which events have been used, for global triggering
this.global[ type ] = true; jQuery.event.global[ type ] = true;
} }
// Nullify elem to prevent memory leaks in IE // Nullify elem to prevent memory leaks in IE
@ -142,43 +121,46 @@ jQuery.event = {
global: {}, global: {},
// Detach an event or set of events from an element // Detach an event or set of events from an element
remove: function( elem, types, handler ) { remove: function( elem, types, handler, pos ) {
// don't do events on text and comment nodes // don't do events on text and comment nodes
if ( elem.nodeType === 3 || elem.nodeType === 8 ) { if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
return; return;
} }
var elemData = jQuery.data( elem ); var ret, type, fn, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType,
elemData = jQuery.data( elem ),
events = elemData && elemData.events;
if ( !elemData ) { if ( !elemData || !events ) {
return; return;
} }
var events = elemData.events, ret, type, fn;
if ( events ) {
// Unbind all events for the element
if ( types === undefined || (typeof types === "string" && types.charAt(0) === ".") ) {
for ( type in events ) {
this.remove( elem, type + (types || "") );
}
} else {
// types is actually an event object here // types is actually an event object here
if ( types.type ) { if ( types && types.type ) {
handler = types.handler; handler = types.handler;
types = types.type; types = types.type;
} }
// Unbind all events for the element
if ( !types || typeof types === "string" && types.charAt(0) === "." ) {
types = types || "";
for ( type in events ) {
jQuery.event.remove( elem, type + types );
}
return;
}
// Handle multiple events separated by a space // Handle multiple events separated by a space
// jQuery(...).unbind("mouseover mouseout", fn); // jQuery(...).unbind("mouseover mouseout", fn);
types = types.split(" "); types = types.split(" ");
var i = 0, all, namespaces, namespace;
while ( (type = types[ i++ ]) ) { while ( (type = types[ i++ ]) ) {
origType = type;
handleObj = null;
all = type.indexOf(".") < 0; all = type.indexOf(".") < 0;
namespaces = null; namespaces = [];
if ( !all ) { if ( !all ) {
// Namespaced event handlers // Namespaced event handlers
@ -187,39 +169,54 @@ jQuery.event = {
namespace = new RegExp("(^|\\.)" + namespace = new RegExp("(^|\\.)" +
jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)") jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)")
} else {
namespaces = [];
} }
var special = this.special[ type ] || {}; eventType = events[ type ];
if ( events[ type ] ) { if ( !eventType ) {
continue;
}
if ( !handler ) {
for ( var j = 0; j < eventType.length; j++ ) {
handleObj = eventType[ j ];
if ( all || namespace.test( handleObj.namespace ) ) {
jQuery.event.remove( elem, origType, handleObj.handler, j );
eventType.splice( j--, 1 );
}
}
continue;
}
special = jQuery.event.special[ type ] || {};
for ( var j = pos || 0; j < eventType.length; j++ ) {
handleObj = eventType[ j ];
if ( handler.guid === handleObj.guid ) {
// remove the given handler for the given type // remove the given handler for the given type
if ( handler ) { if ( all || namespace.test( handleObj.namespace ) ) {
fn = events[ type ][ handler.guid ]; fn = handleObj.handler;
delete events[ type ][ handler.guid ];
// remove all handlers for the given type if ( pos == null ) {
} else { eventType.splice( j--, 1 );
for ( var handle in events[ type ] ) {
// Handle the removal of namespaced events
if ( all || namespace.test( events[ type ][ handle ].type ) ) {
delete events[ type ][ handle ];
}
}
} }
if ( special.remove ) { if ( special.remove ) {
special.remove.call( elem, namespaces, fn); special.remove.call( elem, namespaces, handleObj );
}
}
if ( pos != null ) {
break;
}
}
} }
// remove generic event handler if no more handlers exist // remove generic event handler if no more handlers exist
for ( ret in events[ type ] ) { if ( jQuery.isEmptyObject( events[ type ] ) ) {
break;
}
if ( !ret ) {
if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {
removeEvent( elem, type, elemData.handle ); removeEvent( elem, type, elemData.handle );
} }
@ -228,15 +225,9 @@ jQuery.event = {
delete events[ type ]; delete events[ type ];
} }
} }
}
}
// Remove the expando if it's no longer used // Remove the expando if it's no longer used
for ( ret in events ) { if ( jQuery.isEmptyObject( events ) ) {
break;
}
if ( !ret ) {
var handle = elemData.handle; var handle = elemData.handle;
if ( handle ) { if ( handle ) {
handle.elem = null; handle.elem = null;
@ -249,7 +240,6 @@ jQuery.event = {
jQuery.removeData( elem ); jQuery.removeData( elem );
} }
} }
}
}, },
// bubbling is internal // bubbling is internal
@ -278,7 +268,7 @@ jQuery.event = {
event.stopPropagation(); event.stopPropagation();
// Only trigger if we've ever bound an event for it // Only trigger if we've ever bound an event for it
if ( this.global[ type ] ) { if ( jQuery.event.global[ type ] ) {
jQuery.each( jQuery.cache, function() { jQuery.each( jQuery.cache, function() {
if ( this.events && this.events[type] ) { if ( this.events && this.events[type] ) {
jQuery.event.trigger( event, data, this.handle.elem ); jQuery.event.trigger( event, data, this.handle.elem );
@ -344,7 +334,7 @@ jQuery.event = {
target[ "on" + type ] = null; target[ "on" + type ] = null;
} }
this.triggered = true; jQuery.event.triggered = true;
target[ type ](); target[ type ]();
} }
@ -355,40 +345,43 @@ jQuery.event = {
target[ "on" + type ] = old; target[ "on" + type ] = old;
} }
this.triggered = false; jQuery.event.triggered = false;
} }
} }
}, },
handle: function( event ) { handle: function( event ) {
// returned undefined or false var all, handlers, namespaces, namespace, events;
var all, handlers;
event = arguments[0] = jQuery.event.fix( event || window.event ); event = arguments[0] = jQuery.event.fix( event || window.event );
event.currentTarget = this; event.currentTarget = this;
// Namespaced event handlers // Namespaced event handlers
var namespaces = event.type.split("."); all = event.type.indexOf(".") < 0;
if ( !all ) {
namespaces = event.type.split(".");
event.type = namespaces.shift(); event.type = namespaces.shift();
namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)");
}
// Cache this now, all = true means, any handler var events = jQuery.data(this, "events"), handlers = events[ event.type ];
all = !namespaces.length && !event.exclusive;
var namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)"); if ( events && handlers ) {
// Clone the handlers to prevent manipulation
handlers = handlers.slice(0);
handlers = ( jQuery.data(this, "events") || {} )[ event.type ]; for ( var j = 0, l = handlers.length; j < l; j++ ) {
var handleObj = handlers[ j ];
for ( var j in handlers ) {
var handler = handlers[ j ];
// Filter the functions by class // Filter the functions by class
if ( all || namespace.test(handler.type) ) { if ( (all && !event.exclusive) || namespace.test( handleObj.namespace ) ) {
// Pass in a reference to the handler function itself // Pass in a reference to the handler function itself
// So that we can later remove it // So that we can later remove it
event.handler = handler; event.handler = handleObj.handler;
event.data = handler.data; event.data = handleObj.data;
var ret = handler.apply( this, arguments ); var ret = handleObj.handler.apply( this, arguments );
if ( ret !== undefined ) { if ( ret !== undefined ) {
event.result = ret; event.result = ret;
@ -401,7 +394,7 @@ jQuery.event = {
if ( event.isImmediatePropagationStopped() ) { if ( event.isImmediatePropagationStopped() ) {
break; break;
} }
}
} }
} }

View file

@ -88,8 +88,9 @@ test("bind(), namespace with special add", function() {
}, },
setup: function(){}, setup: function(){},
teardown: function(){}, teardown: function(){},
add: function( handler, data, namespaces ) { add: function( handleObj ) {
return function(e) { var handler = handleObj.handler;
handleObj.handler = function(e) {
e.xyz = ++i; e.xyz = ++i;
handler.apply( this, arguments ); handler.apply( this, arguments );
}; };