events: optimize adding and removing of listeners

These optimizations result in >2x speedup in the ee-add-remove
benchmark:

* Don't mutate array.length when removing the last listener for
an event
* Don't bother checking max listeners if listeners isn't an array
* Don't call delete when removing the last event in _events, just
re-assign a new object instead

PR-URL: https://github.com/iojs/io.js/pull/785
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Evan Lucas <evanlucas@me.com>
This commit is contained in:
Brian White 2015-02-11 17:00:12 -05:00 committed by Ben Noordhuis
parent 630f636334
commit 7061669dba

View File

@ -30,8 +30,10 @@ EventEmitter.init = function() {
} }
} }
if (!this._events || this._events === Object.getPrototypeOf(this)._events) if (!this._events || this._events === Object.getPrototypeOf(this)._events) {
this._events = {}; this._events = {};
this._eventsCount = 0;
}
this._maxListeners = this._maxListeners || undefined; this._maxListeners = this._maxListeners || undefined;
}; };
@ -115,15 +117,18 @@ function emitMany(handler, isFn, self, args) {
EventEmitter.prototype.emit = function emit(type) { EventEmitter.prototype.emit = function emit(type) {
var er, handler, len, args, i, events, domain; var er, handler, len, args, i, events, domain;
var needDomainExit = false; var needDomainExit = false;
var doError = (type === 'error');
events = this._events; events = this._events;
if (!events) if (events)
events = this._events = {}; doError = (doError && events.error == null);
else if (!doError)
return false;
domain = this.domain; domain = this.domain;
// If there is no 'error' event listener then throw. // If there is no 'error' event listener then throw.
if (type === 'error' && !events.error) { if (doError) {
er = arguments[1]; er = arguments[1];
if (domain) { if (domain) {
if (!er) if (!er)
@ -189,31 +194,38 @@ EventEmitter.prototype.addListener = function addListener(type, listener) {
throw new TypeError('listener must be a function'); throw new TypeError('listener must be a function');
events = this._events; events = this._events;
if (!events) if (!events) {
events = this._events = {}; events = this._events = {};
else { this._eventsCount = 0;
} else {
// To avoid recursion in the case that type === "newListener"! Before // To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener". // adding it to the listeners, first emit "newListener".
if (events.newListener) { if (events.newListener) {
this.emit('newListener', type, this.emit('newListener', type,
typeof listener.listener === 'function' ? listener.listener ? listener.listener : listener);
listener.listener : listener);
// Re-assign `events` because a newListener handler could have caused the
// this._events to be assigned to a new object
events = this._events;
} }
existing = events[type]; existing = events[type];
} }
if (!existing) if (!existing) {
// Optimize the case of one listener. Don't need the extra array object. // Optimize the case of one listener. Don't need the extra array object.
existing = events[type] = listener; existing = events[type] = listener;
else if (typeof existing !== 'function') ++this._eventsCount;
// If we've already got an array, just append. } else {
existing.push(listener); if (typeof existing === 'function') {
else
// Adding the second element, need to change to array. // Adding the second element, need to change to array.
existing = events[type] = [existing, listener]; existing = events[type] = [existing, listener];
} else {
// If we've already got an array, just append.
existing.push(listener);
}
// Check for listener leak // Check for listener leak
if (typeof existing !== 'function' && !existing.warned) { if (!existing.warned) {
m = $getMaxListeners(this); m = $getMaxListeners(this);
if (m && m > 0 && existing.length > m) { if (m && m > 0 && existing.length > m) {
existing.warned = true; existing.warned = true;
@ -224,6 +236,7 @@ EventEmitter.prototype.addListener = function addListener(type, listener) {
console.trace(); console.trace();
} }
} }
}
return this; return this;
}; };
@ -254,7 +267,7 @@ EventEmitter.prototype.once = function once(type, listener) {
// emits a 'removeListener' event iff the listener was removed // emits a 'removeListener' event iff the listener was removed
EventEmitter.prototype.removeListener = EventEmitter.prototype.removeListener =
function removeListener(type, listener) { function removeListener(type, listener) {
var list, events, position, length, i; var list, events, position, i;
if (typeof listener !== 'function') if (typeof listener !== 'function')
throw new TypeError('listener must be a function'); throw new TypeError('listener must be a function');
@ -267,17 +280,18 @@ EventEmitter.prototype.removeListener =
if (!list) if (!list)
return this; return this;
length = list.length; if (list === listener || (list.listener && list.listener === listener)) {
position = -1; if (--this._eventsCount === 0)
this._events = {};
if (list === listener || else {
(typeof list.listener === 'function' && list.listener === listener)) {
delete events[type]; delete events[type];
if (events.removeListener) if (events.removeListener)
this.emit('removeListener', type, listener); this.emit('removeListener', type, listener);
}
} else if (typeof list !== 'function') { } else if (typeof list !== 'function') {
for (i = length; i-- > 0;) { position = -1;
for (i = list.length; i-- > 0;) {
if (list[i] === listener || if (list[i] === listener ||
(list[i].listener && list[i].listener === listener)) { (list[i].listener && list[i].listener === listener)) {
position = i; position = i;
@ -289,7 +303,11 @@ EventEmitter.prototype.removeListener =
return this; return this;
if (list.length === 1) { if (list.length === 1) {
list.length = 0; list[0] = undefined;
if (--this._eventsCount === 0) {
this._events = {};
return this;
} else
delete events[type]; delete events[type];
} else { } else {
spliceOne(list, position); spliceOne(list, position);
@ -312,10 +330,15 @@ EventEmitter.prototype.removeAllListeners =
// not listening for removeListener, no need to emit // not listening for removeListener, no need to emit
if (!events.removeListener) { if (!events.removeListener) {
if (arguments.length === 0) if (arguments.length === 0) {
this._events = {}; this._events = {};
else if (events[type]) this._eventsCount = 0;
} else if (events[type]) {
if (--this._eventsCount === 0)
this._events = {};
else
delete events[type]; delete events[type];
}
return this; return this;
} }
@ -329,6 +352,7 @@ EventEmitter.prototype.removeAllListeners =
} }
this.removeAllListeners('removeListener'); this.removeAllListeners('removeListener');
this._events = {}; this._events = {};
this._eventsCount = 0;
return this; return this;
} }
@ -336,12 +360,12 @@ EventEmitter.prototype.removeAllListeners =
if (typeof listeners === 'function') { if (typeof listeners === 'function') {
this.removeListener(type, listeners); this.removeListener(type, listeners);
} else if (Array.isArray(listeners)) { } else if (listeners) {
// LIFO order // LIFO order
while (listeners.length) do {
this.removeListener(type, listeners[listeners.length - 1]); this.removeListener(type, listeners[listeners.length - 1]);
} while (listeners[0]);
} }
delete events[type];
return this; return this;
}; };