From 7061669dba26337e47e258385dd58e400881dff1 Mon Sep 17 00:00:00 2001 From: Brian White Date: Wed, 11 Feb 2015 17:00:12 -0500 Subject: [PATCH] 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 Reviewed-By: Evan Lucas --- lib/events.js | 114 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 45 deletions(-) diff --git a/lib/events.js b/lib/events.js index 33bcc185337079..02e7ac951463c0 100644 --- a/lib/events.js +++ b/lib/events.js @@ -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; }; @@ -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) @@ -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(); + } } } @@ -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'); @@ -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; @@ -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); } @@ -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; } @@ -329,6 +352,7 @@ EventEmitter.prototype.removeAllListeners = } this.removeAllListeners('removeListener'); this._events = {}; + this._eventsCount = 0; return this; } @@ -336,12 +360,12 @@ EventEmitter.prototype.removeAllListeners = 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; };