Skip to content

Commit

Permalink
events: optimize adding and removing of listeners
Browse files Browse the repository at this point in the history
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: #785
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Evan Lucas <evanlucas@me.com>
  • Loading branch information
mscdex authored and bnoordhuis committed Feb 11, 2015
1 parent 630f636 commit 7061669
Showing 1 changed file with 69 additions and 45 deletions.
114 changes: 69 additions & 45 deletions lib/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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._eventsCount = 0;
}

this._maxListeners = this._maxListeners || undefined;
};
Expand Down Expand Up @@ -115,15 +117,18 @@ function emitMany(handler, isFn, self, args) {
EventEmitter.prototype.emit = function emit(type) {
var er, handler, len, args, i, events, domain;
var needDomainExit = false;
var doError = (type === 'error');

events = this._events;
if (!events)
events = this._events = {};
if (events)
doError = (doError && events.error == null);
else if (!doError)
return false;

domain = this.domain;

// If there is no 'error' event listener then throw.
if (type === 'error' && !events.error) {
if (doError) {
er = arguments[1];
if (domain) {
if (!er)
Expand Down Expand Up @@ -189,39 +194,47 @@ EventEmitter.prototype.addListener = function addListener(type, listener) {
throw new TypeError('listener must be a function');

events = this._events;
if (!events)
if (!events) {
events = this._events = {};
else {
this._eventsCount = 0;
} else {
// To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener".
if (events.newListener) {
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];
}

if (!existing)
if (!existing) {
// Optimize the case of one listener. Don't need the extra array object.
existing = events[type] = listener;
else if (typeof existing !== 'function')
// If we've already got an array, just append.
existing.push(listener);
else
// Adding the second element, need to change to array.
existing = events[type] = [existing, listener];

// Check for listener leak
if (typeof existing !== 'function' && !existing.warned) {
m = $getMaxListeners(this);
if (m && m > 0 && existing.length > m) {
existing.warned = true;
console.error('(node) warning: possible EventEmitter memory ' +
'leak detected. %d %s listeners added. ' +
'Use emitter.setMaxListeners() to increase limit.',
existing.length, type);
console.trace();
++this._eventsCount;
} else {
if (typeof existing === 'function') {
// Adding the second element, need to change to array.
existing = events[type] = [existing, listener];
} else {
// If we've already got an array, just append.
existing.push(listener);
}

// Check for listener leak
if (!existing.warned) {
m = $getMaxListeners(this);
if (m && m > 0 && existing.length > m) {
existing.warned = true;
console.error('(node) warning: possible EventEmitter memory ' +
'leak detected. %d %s listeners added. ' +
'Use emitter.setMaxListeners() to increase limit.',
existing.length, type);
console.trace();
}
}
}

Expand Down Expand Up @@ -254,7 +267,7 @@ EventEmitter.prototype.once = function once(type, listener) {
// emits a 'removeListener' event iff the listener was removed
EventEmitter.prototype.removeListener =
function removeListener(type, listener) {
var list, events, position, length, i;
var list, events, position, i;

if (typeof listener !== 'function')
throw new TypeError('listener must be a function');
Expand All @@ -267,17 +280,18 @@ EventEmitter.prototype.removeListener =
if (!list)
return this;

length = list.length;
position = -1;

if (list === listener ||
(typeof list.listener === 'function' && list.listener === listener)) {
delete events[type];
if (events.removeListener)
this.emit('removeListener', type, listener);

if (list === listener || (list.listener && list.listener === listener)) {
if (--this._eventsCount === 0)
this._events = {};
else {
delete events[type];
if (events.removeListener)
this.emit('removeListener', type, listener);
}
} else if (typeof list !== 'function') {
for (i = length; i-- > 0;) {
position = -1;

for (i = list.length; i-- > 0;) {
if (list[i] === listener ||
(list[i].listener && list[i].listener === listener)) {
position = i;
Expand All @@ -289,8 +303,12 @@ EventEmitter.prototype.removeListener =
return this;

if (list.length === 1) {
list.length = 0;
delete events[type];
list[0] = undefined;
if (--this._eventsCount === 0) {
this._events = {};
return this;
} else
delete events[type];
} else {
spliceOne(list, position);
}
Expand All @@ -312,10 +330,15 @@ EventEmitter.prototype.removeAllListeners =

// not listening for removeListener, no need to emit
if (!events.removeListener) {
if (arguments.length === 0)
if (arguments.length === 0) {
this._events = {};
else if (events[type])
delete events[type];
this._eventsCount = 0;
} else if (events[type]) {
if (--this._eventsCount === 0)
this._events = {};
else
delete events[type];
}
return this;
}

Expand All @@ -329,19 +352,20 @@ EventEmitter.prototype.removeAllListeners =
}
this.removeAllListeners('removeListener');
this._events = {};
this._eventsCount = 0;
return this;
}

listeners = events[type];

if (typeof listeners === 'function') {
this.removeListener(type, listeners);
} else if (Array.isArray(listeners)) {
} else if (listeners) {
// LIFO order
while (listeners.length)
do {
this.removeListener(type, listeners[listeners.length - 1]);
} while (listeners[0]);
}
delete events[type];

return this;
};
Expand Down

0 comments on commit 7061669

Please sign in to comment.