diff --git a/benchmark/events/ee-add-once.js b/benchmark/events/ee-add-once.js new file mode 100644 index 00000000000000..7ac91a8f7d7b3e --- /dev/null +++ b/benchmark/events/ee-add-once.js @@ -0,0 +1,35 @@ +'use strict'; + +var common = require('../common'); +var EventEmitter = require('events'); +var v8 = require('v8'); + +var bench = common.createBenchmark(main, { + n: [50e6] +}); + +function main(conf) { + var n = +conf.n; + var ee = new EventEmitter(); + var listeners = []; + + var k; + for (k = 0; k < 10; k += 1) + listeners.push(function() {}); + + // Force optimization before starting the benchmark + ee.once('dummy', listeners[0]); + ee._events = {}; + v8.setFlagsFromString('--allow_natives_syntax'); + eval('%OptimizeFunctionOnNextCall(ee.once)'); + ee.once('dummy', listeners[0]); + ee._events = {}; + + bench.start(); + for (var i = 0; i < n; ++i) { + for (k = listeners.length; --k >= 0; /* empty */) + ee.once('dummy', listeners[k]); + ee._events = {}; + } + bench.end(n); +} diff --git a/benchmark/events/ee-add-remove-once.js b/benchmark/events/ee-add-remove-once.js new file mode 100644 index 00000000000000..7f8edfa9d15bd1 --- /dev/null +++ b/benchmark/events/ee-add-remove-once.js @@ -0,0 +1,41 @@ +'use strict'; + +var common = require('../common'); +var EventEmitter = require('events'); +var v8 = require('v8'); + +var bench = common.createBenchmark(main, { + n: [25e6] +}); + +function main(conf) { + var n = conf.n | 0; + + var ee = new EventEmitter(); + var listeners = []; + + var k; + for (k = 0; k < 10; k += 1) + listeners.push(function() {}); + + for (k = listeners.length; --k >= 0; /* empty */) + ee.once('dummy', listeners[k]); + for (k = listeners.length; --k >= 0; /* empty */) + ee.removeListener('dummy', listeners[k]); + v8.setFlagsFromString('--allow_natives_syntax'); + eval('%OptimizeFunctionOnNextCall(ee.once)'); + eval('%OptimizeFunctionOnNextCall(ee.removeListener)'); + for (k = listeners.length; --k >= 0; /* empty */) + ee.once('dummy', listeners[k]); + for (k = listeners.length; --k >= 0; /* empty */) + ee.removeListener('dummy', listeners[k]); + + bench.start(); + for (var i = 0; i < n; i += 1) { + for (k = listeners.length; --k >= 0; /* empty */) + ee.once('dummy', listeners[k]); + for (k = listeners.length; --k >= 0; /* empty */) + ee.removeListener('dummy', listeners[k]); + } + bench.end(n); +} diff --git a/benchmark/events/ee-add-remove.js b/benchmark/events/ee-add-remove.js index 99d85367cb8d6f..0fbdccd566d24d 100644 --- a/benchmark/events/ee-add-remove.js +++ b/benchmark/events/ee-add-remove.js @@ -1,19 +1,35 @@ 'use strict'; -var common = require('../common.js'); -var events = require('events'); -var bench = common.createBenchmark(main, {n: [25e4]}); +var common = require('../common'); +var EventEmitter = require('events'); +var v8 = require('v8'); + +var bench = common.createBenchmark(main, { + n: [50e5] +}); function main(conf) { var n = conf.n | 0; - var ee = new events.EventEmitter(); + var ee = new EventEmitter(); var listeners = []; var k; for (k = 0; k < 10; k += 1) listeners.push(function() {}); + for (k = listeners.length; --k >= 0; /* empty */) + ee.on('dummy', listeners[k]); + for (k = listeners.length; --k >= 0; /* empty */) + ee.removeListener('dummy', listeners[k]); + v8.setFlagsFromString('--allow_natives_syntax'); + eval('%OptimizeFunctionOnNextCall(ee.on)'); + eval('%OptimizeFunctionOnNextCall(ee.removeListener)'); + for (k = listeners.length; --k >= 0; /* empty */) + ee.on('dummy', listeners[k]); + for (k = listeners.length; --k >= 0; /* empty */) + ee.removeListener('dummy', listeners[k]); + bench.start(); for (var i = 0; i < n; i += 1) { for (k = listeners.length; --k >= 0; /* empty */) diff --git a/benchmark/events/ee-add.js b/benchmark/events/ee-add.js new file mode 100644 index 00000000000000..bdd61b6302e589 --- /dev/null +++ b/benchmark/events/ee-add.js @@ -0,0 +1,35 @@ +'use strict'; + +var common = require('../common'); +var EventEmitter = require('events'); +var v8 = require('v8'); + +var bench = common.createBenchmark(main, { + n: [100e5] +}); + +function main(conf) { + var n = +conf.n; + var ee = new EventEmitter(); + var listeners = []; + + var k; + for (k = 0; k < 10; k += 1) + listeners.push(function() {}); + + // Force optimization before starting the benchmark + ee.on('dummy', listeners[0]); + ee._events = {}; + v8.setFlagsFromString('--allow_natives_syntax'); + eval('%OptimizeFunctionOnNextCall(ee.on)'); + ee.on('dummy', listeners[0]); + ee._events = {}; + + bench.start(); + for (var i = 0; i < n; ++i) { + for (k = listeners.length; --k >= 0; /* empty */) + ee.on('dummy', listeners[k]); + ee._events = {}; + } + bench.end(n); +} diff --git a/benchmark/events/ee-emit-multi-args.js b/benchmark/events/ee-emit-multi-args.js index b423c216b1ed73..bc244adabd4b14 100644 --- a/benchmark/events/ee-emit-multi-args.js +++ b/benchmark/events/ee-emit-multi-args.js @@ -1,8 +1,12 @@ 'use strict'; -var common = require('../common.js'); -var EventEmitter = require('events').EventEmitter; -var bench = common.createBenchmark(main, {n: [2e6]}); +var common = require('../common'); +var EventEmitter = require('events'); +var v8 = require('v8'); + +var bench = common.createBenchmark(main, { + n: [25e6] +}); function main(conf) { var n = conf.n | 0; @@ -12,6 +16,11 @@ function main(conf) { for (var k = 0; k < 10; k += 1) ee.on('dummy', function() {}); + ee.emit('dummy', 5, true); + v8.setFlagsFromString('--allow_natives_syntax'); + eval('%OptimizeFunctionOnNextCall(ee.emit)'); + ee.emit('dummy', 5, true); + bench.start(); for (var i = 0; i < n; i += 1) { ee.emit('dummy', 5, true); diff --git a/benchmark/events/ee-emit-once.js b/benchmark/events/ee-emit-once.js new file mode 100644 index 00000000000000..2747bd8aa93b7e --- /dev/null +++ b/benchmark/events/ee-emit-once.js @@ -0,0 +1,32 @@ +'use strict'; + +var common = require('../common'); +var EventEmitter = require('events'); +var v8 = require('v8'); + +var bench = common.createBenchmark(main, { + n: [25e6] +}); + +function main(conf) { + var n = conf.n | 0; + + var ee = new EventEmitter(); + + function noop() {} + + ee.once('dummy', noop); + ee.emit('dummy'); + v8.setFlagsFromString('--allow_natives_syntax'); + eval('%OptimizeFunctionOnNextCall(ee.once)'); + eval('%OptimizeFunctionOnNextCall(ee.emit)'); + ee.once('dummy', noop); + ee.emit('dummy'); + + bench.start(); + for (var i = 0; i < n; i += 1) { + ee.once('dummy', noop); + ee.emit('dummy'); + } + bench.end(n); +} diff --git a/benchmark/events/ee-emit.js b/benchmark/events/ee-emit.js index 87772222f0a467..622d95e3e6f5d5 100644 --- a/benchmark/events/ee-emit.js +++ b/benchmark/events/ee-emit.js @@ -1,8 +1,12 @@ 'use strict'; -var common = require('../common.js'); -var EventEmitter = require('events').EventEmitter; -var bench = common.createBenchmark(main, {n: [2e6]}); +var common = require('../common'); +var EventEmitter = require('events'); +var v8 = require('v8'); + +var bench = common.createBenchmark(main, { + n: [50e6] +}); function main(conf) { var n = conf.n | 0; @@ -12,6 +16,11 @@ function main(conf) { for (var k = 0; k < 10; k += 1) ee.on('dummy', function() {}); + ee.emit('dummy'); + v8.setFlagsFromString('--allow_natives_syntax'); + eval('%OptimizeFunctionOnNextCall(ee.emit)'); + ee.emit('dummy'); + bench.start(); for (var i = 0; i < n; i += 1) { ee.emit('dummy'); diff --git a/benchmark/events/ee-listener-count-on-prototype.js b/benchmark/events/ee-listener-count-on-prototype.js index dfe7e27cd09144..bb489847a5d9c8 100644 --- a/benchmark/events/ee-listener-count-on-prototype.js +++ b/benchmark/events/ee-listener-count-on-prototype.js @@ -1,8 +1,12 @@ 'use strict'; -var common = require('../common.js'); -var EventEmitter = require('events').EventEmitter; -var bench = common.createBenchmark(main, {n: [5e7]}); +var common = require('../common'); +var EventEmitter = require('events'); +var v8 = require('v8'); + +var bench = common.createBenchmark(main, { + n: [5e7] +}); function main(conf) { var n = conf.n | 0; @@ -12,6 +16,11 @@ function main(conf) { for (var k = 0; k < 10; k += 1) ee.on('dummy', function() {}); + ee.listenerCount('dummy'); + v8.setFlagsFromString('--allow_natives_syntax'); + eval('%OptimizeFunctionOnNextCall(ee.listenerCount)'); + ee.listenerCount('dummy'); + bench.start(); for (var i = 0; i < n; i += 1) { ee.listenerCount('dummy'); diff --git a/benchmark/events/ee-listeners-many.js b/benchmark/events/ee-listeners-many.js index 063732e1facb4b..2c7af56ba3b7b5 100644 --- a/benchmark/events/ee-listeners-many.js +++ b/benchmark/events/ee-listeners-many.js @@ -1,8 +1,12 @@ 'use strict'; -var common = require('../common.js'); -var EventEmitter = require('events').EventEmitter; -var bench = common.createBenchmark(main, {n: [5e6]}); +var common = require('../common'); +var EventEmitter = require('events'); +var v8 = require('v8'); + +var bench = common.createBenchmark(main, { + n: [15e6] +}); function main(conf) { var n = conf.n | 0; @@ -13,6 +17,11 @@ function main(conf) { for (var k = 0; k < 100; k += 1) ee.on('dummy', function() {}); + ee.listeners('dummy'); + v8.setFlagsFromString('--allow_natives_syntax'); + eval('%OptimizeFunctionOnNextCall(ee.listeners)'); + ee.listeners('dummy'); + bench.start(); for (var i = 0; i < n; i += 1) { ee.listeners('dummy'); diff --git a/benchmark/events/ee-listeners.js b/benchmark/events/ee-listeners.js index e91ca5078f5f98..fa4380b27f1f09 100644 --- a/benchmark/events/ee-listeners.js +++ b/benchmark/events/ee-listeners.js @@ -1,8 +1,12 @@ 'use strict'; -var common = require('../common.js'); -var EventEmitter = require('events').EventEmitter; -var bench = common.createBenchmark(main, {n: [5e6]}); +var common = require('../common'); +var EventEmitter = require('events'); +var v8 = require('v8'); + +var bench = common.createBenchmark(main, { + n: [90e6] +}); function main(conf) { var n = conf.n | 0; @@ -12,6 +16,11 @@ function main(conf) { for (var k = 0; k < 10; k += 1) ee.on('dummy', function() {}); + ee.listeners('dummy'); + v8.setFlagsFromString('--allow_natives_syntax'); + eval('%OptimizeFunctionOnNextCall(ee.listeners)'); + ee.listeners('dummy'); + bench.start(); for (var i = 0; i < n; i += 1) { ee.listeners('dummy'); diff --git a/benchmark/events/ee-remove-all-once.js b/benchmark/events/ee-remove-all-once.js new file mode 100644 index 00000000000000..b4a55a50edf668 --- /dev/null +++ b/benchmark/events/ee-remove-all-once.js @@ -0,0 +1,84 @@ +'use strict'; + +var common = require('../common'); +var EventEmitter = require('events'); +var v8 = require('v8'); + +var bench = common.createBenchmark(main, { + n: [50e6], + withType: ['true', 'false'] +}); + +function main(conf) { + var n = +conf.n; + var ee = new EventEmitter(); + var isNewEE = (function() { + var ee2 = new EventEmitter(); + ee2.once('foo', noop); + ee2.once('foo', noop); + return (typeof ee2._events.foo.onceCount === 'number'); + })(); + + function noop() {} + + function onceWrap(listener) { + var fired = false; + function g() { + this.removeListener('dummy', g); + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + g.listener = listener; + return g; + } + + function setListeners() { + var arr = new Array(10); + var i; + if (isNewEE) { + for (i = 0; i < arr.length; ++i) + arr[i] = { once: noop, fired: false }; + arr.onceCount = arr.length; + } else { + for (i = 0; i < arr.length; ++i) + arr[i] = onceWrap(noop); + } + ee._events.dummy = arr; + ee._eventsCount = 1; + } + + // Force optimization before starting the benchmark + setListeners(); + if (conf.withType === 'true') + ee.removeAllListeners('dummy'); + else + ee.removeAllListeners(); + v8.setFlagsFromString('--allow_natives_syntax'); + eval('%OptimizeFunctionOnNextCall(ee.removeAllListeners)'); + eval('%OptimizeFunctionOnNextCall(setListeners)'); + setListeners(); + if (conf.withType === 'true') + ee.removeAllListeners('dummy'); + else + ee.removeAllListeners(); + + var i; + if (conf.withType === 'true') { + bench.start(); + for (i = 0; i < n; ++i) { + setListeners(); + ee.removeAllListeners('dummy'); + } + bench.end(n); + } else { + bench.start(); + for (i = 0; i < n; ++i) { + setListeners(); + ee.removeAllListeners(); + } + bench.end(n); + } +} + diff --git a/benchmark/events/ee-remove-all.js b/benchmark/events/ee-remove-all.js new file mode 100644 index 00000000000000..6aac060a828a30 --- /dev/null +++ b/benchmark/events/ee-remove-all.js @@ -0,0 +1,58 @@ +'use strict'; + +var common = require('../common'); +var EventEmitter = require('events'); +var v8 = require('v8'); + +var bench = common.createBenchmark(main, { + n: [60e6], + withType: ['true', 'false'] +}); + +function main(conf) { + var n = +conf.n; + var ee = new EventEmitter(); + + function noop() {} + + function setListeners() { + var arr = new Array(10); + for (var i = 0; i < arr.length; ++i) + arr[i] = noop; + arr.onceCount = 0; + ee._events.dummy = arr; + ee._eventsCount = 1; + } + + // Force optimization before starting the benchmark + setListeners(); + if (conf.withType === 'true') + ee.removeAllListeners('dummy'); + else + ee.removeAllListeners(); + v8.setFlagsFromString('--allow_natives_syntax'); + eval('%OptimizeFunctionOnNextCall(setListeners)'); + eval('%OptimizeFunctionOnNextCall(ee.removeAllListeners)'); + setListeners(); + if (conf.withType === 'true') + ee.removeAllListeners('dummy'); + else + ee.removeAllListeners(); + + var i; + if (conf.withType === 'true') { + bench.start(); + for (i = 0; i < n; ++i) { + setListeners(); + ee.removeAllListeners('dummy'); + } + bench.end(n); + } else { + bench.start(); + for (i = 0; i < n; ++i) { + setListeners(); + ee.removeAllListeners(); + } + bench.end(n); + } +} diff --git a/benchmark/events/ee-remove.js b/benchmark/events/ee-remove.js new file mode 100644 index 00000000000000..5e8bb6c3486464 --- /dev/null +++ b/benchmark/events/ee-remove.js @@ -0,0 +1,47 @@ +'use strict'; + +var common = require('../common'); +var EventEmitter = require('events'); +var v8 = require('v8'); + +var bench = common.createBenchmark(main, { + n: [20e6] +}); + +function main(conf) { + var n = +conf.n; + var ee = new EventEmitter(); + var listenerCount = 10; + + function noop() {} + + function setListeners() { + var arr = new Array(listenerCount); + for (var i = 0; i < arr.length; ++i) + arr[i] = noop; + arr.onceCount = 0; + ee._events.dummy = arr; + ee._eventsCount = 1; + } + + var k; + + // Force optimization before starting the benchmark + setListeners(); + for (k = listenerCount; --k >= 0; /* empty */) + ee.removeListener('dummy', noop); + v8.setFlagsFromString('--allow_natives_syntax'); + eval('%OptimizeFunctionOnNextCall(ee.removeListener)'); + eval('%OptimizeFunctionOnNextCall(setListeners)'); + setListeners(); + for (k = listenerCount; --k >= 0; /* empty */) + ee.removeListener('dummy', noop); + + bench.start(); + for (var i = 0; i < n; ++i) { + setListeners(); + for (k = listenerCount; --k >= 0; /* empty */) + ee.removeListener('dummy', noop); + } + bench.end(n); +} diff --git a/lib/_stream_readable.js b/lib/_stream_readable.js index 1ad85dc81c8b5c..ac2a0336c721bf 100644 --- a/lib/_stream_readable.js +++ b/lib/_stream_readable.js @@ -562,10 +562,16 @@ Readable.prototype.pipe = function(dest, pipeOpts) { // is attached before any userland ones. NEVER DO THIS. if (!dest._events || !dest._events.error) dest.on('error', onerror); - else if (Array.isArray(dest._events.error)) - dest._events.error.unshift(onerror); - else - dest._events.error = [onerror, dest._events.error]; + else { + var existingHandler = dest._events.error; + if (Array.isArray(existingHandler)) + existingHandler.unshift(onerror); + else { + var handlers = [onerror, existingHandler]; + handlers.onceCount = (existingHandler.once ? 1 : 0); + dest._events.error = handlers; + } + } // Both close and finish should trigger unpipe, but only once. @@ -669,9 +675,7 @@ Readable.prototype.unpipe = function(dest) { // set up data events if they are asked for // Ensure readable listeners eventually get something -Readable.prototype.on = function(ev, fn) { - var res = Stream.prototype.on.call(this, ev, fn); - +Readable.prototype._on = function(ev) { // If listening to data, and it has not explicitly been paused, // then call resume to start the flow of data on the next tick. if (ev === 'data' && false !== this._readableState.flowing) { @@ -691,7 +695,15 @@ Readable.prototype.on = function(ev, fn) { } } } - +}; +Readable.prototype.on = function(ev, fn) { + var res = Stream.prototype.on.call(this, ev, fn); + this._on(ev); + return res; +}; +Readable.prototype.once = function(ev, fn) { + var res = Stream.prototype.once.call(this, ev, fn); + this._on(ev); return res; }; Readable.prototype.addListener = Readable.prototype.on; diff --git a/lib/events.js b/lib/events.js index 5860254654d8f1..df830d63b0233f 100644 --- a/lib/events.js +++ b/lib/events.js @@ -86,6 +86,38 @@ function emitNone(handler, isFn, self) { listeners[i].call(self); } } +function emitNoneOnce(handler, isFn, self, ev) { + var fired; + if (isFn) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = handler.once; + self.removeListener(ev, handler); + if (fired) + return; + handler.call(self); + } else { + fired = false; + var len = handler.length; + var listeners = arrayClone(handler, len); + var once; + for (var i = 0; i < len; ++i) { + handler = listeners[i]; + once = handler.once; + if (once !== undefined) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = once; + self.removeListener(ev, handler); + if (fired) + continue; + } + handler.call(self); + } + } +} function emitOne(handler, isFn, self, arg1) { if (isFn) handler.call(self, arg1); @@ -96,6 +128,38 @@ function emitOne(handler, isFn, self, arg1) { listeners[i].call(self, arg1); } } +function emitOneOnce(handler, isFn, self, ev, arg1) { + var fired; + if (isFn) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = handler.once; + self.removeListener(ev, handler); + if (fired) + return; + handler.call(self, arg1); + } else { + fired = false; + var len = handler.length; + var listeners = arrayClone(handler, len); + var once; + for (var i = 0; i < len; ++i) { + handler = listeners[i]; + once = handler.once; + if (once) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = once; + self.removeListener(ev, handler); + if (fired) + continue; + } + handler.call(self, arg1); + } + } +} function emitTwo(handler, isFn, self, arg1, arg2) { if (isFn) handler.call(self, arg1, arg2); @@ -106,6 +170,38 @@ function emitTwo(handler, isFn, self, arg1, arg2) { listeners[i].call(self, arg1, arg2); } } +function emitTwoOnce(handler, isFn, self, ev, arg1, arg2) { + var fired; + if (isFn) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = handler.once; + self.removeListener(ev, handler); + if (fired) + return; + handler.call(self, arg1, arg2); + } else { + fired = false; + var len = handler.length; + var listeners = arrayClone(handler, len); + var once; + for (var i = 0; i < len; ++i) { + handler = listeners[i]; + once = handler.once; + if (once) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = once; + self.removeListener(ev, handler); + if (fired) + continue; + } + handler.call(self, arg1, arg2); + } + } +} function emitThree(handler, isFn, self, arg1, arg2, arg3) { if (isFn) handler.call(self, arg1, arg2, arg3); @@ -116,7 +212,38 @@ function emitThree(handler, isFn, self, arg1, arg2, arg3) { listeners[i].call(self, arg1, arg2, arg3); } } - +function emitThreeOnce(handler, isFn, self, ev, arg1, arg2, arg3) { + var fired; + if (isFn) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = handler.once; + self.removeListener(ev, handler); + if (fired) + return; + handler.call(self, arg1, arg2, arg3); + } else { + fired = false; + var len = handler.length; + var listeners = arrayClone(handler, len); + var once; + for (var i = 0; i < len; ++i) { + handler = listeners[i]; + once = handler.once; + if (once) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = once; + self.removeListener(ev, handler); + if (fired) + continue; + } + handler.call(self, arg1, arg2, arg3); + } + } +} function emitMany(handler, isFn, self, args) { if (isFn) handler.apply(self, args); @@ -127,23 +254,57 @@ function emitMany(handler, isFn, self, args) { listeners[i].apply(self, args); } } +function emitManyOnce(handler, isFn, self, ev, args) { + var fired; + if (isFn) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = handler.once; + self.removeListener(ev, handler); + if (fired) + return; + handler.apply(self, args); + } else { + fired = false; + var len = handler.length; + var listeners = arrayClone(handler, len); + var once; + for (var i = 0; i < len; ++i) { + handler = listeners[i]; + once = handler.once; + if (once) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = once; + self.removeListener(ev, handler); + if (fired) + continue; + } + handler.apply(self, args); + } + } +} EventEmitter.prototype.emit = function emit(type) { - var er, handler, len, args, i, events, domain; + var i, j; + var isArray = false; + var useOnce = false; var needDomainExit = false; var doError = (type === 'error'); - events = this._events; - if (events) - doError = (doError && events.error == null); + const events = this._events; + if (events !== undefined) + doError = (doError && events.error === undefined); else if (!doError) return false; - domain = this.domain; + const domain = this.domain; // If there is no 'error' event listener then throw. if (doError) { - er = arguments[1]; + var er = arguments[1]; if (domain) { if (!er) er = new Error('Uncaught, unspecified "error" event'); @@ -162,38 +323,62 @@ EventEmitter.prototype.emit = function emit(type) { return false; } - handler = events[type]; + const handler = events[type]; - if (!handler) + if (handler === undefined) return false; - if (domain && this !== process) { + if (domain != null && this !== process) { domain.enter(); needDomainExit = true; } - var isFn = typeof handler === 'function'; - len = arguments.length; + const len = arguments.length; + + if (typeof handler !== 'function') { + const onceCount = handler.onceCount; + isArray = (typeof handler.once !== 'function'); + useOnce = (!isArray || onceCount > 0); + } + switch (len) { // fast cases case 1: - emitNone(handler, isFn, this); + if (!useOnce) + emitNone(handler, !isArray, this); + else + emitNoneOnce(handler, !isArray, this, type); break; case 2: - emitOne(handler, isFn, this, arguments[1]); + if (!useOnce) + emitOne(handler, !isArray, this, arguments[1]); + else + emitOneOnce(handler, !isArray, this, type, arguments[1]); break; case 3: - emitTwo(handler, isFn, this, arguments[1], arguments[2]); + if (!useOnce) + emitTwo(handler, !isArray, this, arguments[1], arguments[2]); + else + emitTwoOnce(handler, !isArray, this, type, arguments[1], arguments[2]); break; case 4: - emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); + if (!useOnce) { + emitThree(handler, !isArray, this, arguments[1], arguments[2], + arguments[3]); + } else { + emitThreeOnce(handler, !isArray, this, type, arguments[1], arguments[2], + arguments[3]); + } break; // slower default: - args = new Array(len - 1); - for (i = 1; i < len; i++) - args[i - 1] = arguments[i]; - emitMany(handler, isFn, this, args); + var args = new Array(len - 1); + for (i = 1, j = 0; i < len; ++i, ++j) + args[j] = arguments[i]; + if (!useOnce) + emitMany(handler, !isArray, this, args); + else + emitManyOnce(handler, !isArray, this, type, args); } if (needDomainExit) @@ -202,8 +387,7 @@ EventEmitter.prototype.emit = function emit(type) { return true; }; -EventEmitter.prototype.addListener = function addListener(type, listener) { - var m; +EventEmitter.prototype.on = function on(type, listener) { var events; var existing; @@ -218,11 +402,10 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { // 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, - listener.listener ? listener.listener : listener); + this.emit('newListener', type, listener); - // Re-assign `events` because a newListener handler could have caused the - // this._events to be assigned to a new object + // Re-assign `events` because a newListener handler could have caused + // `this._events` to be assigned to a new object events = this._events; } existing = events[type]; @@ -236,6 +419,11 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { if (typeof existing === 'function') { // Adding the second element, need to change to array. existing = events[type] = [existing, listener]; + } else if (typeof existing.once === 'function') { + // Adding the second element, need to change to array. Existing listener + // was a one-time listener. + existing = events[type] = [existing, listener]; + existing.onceCount = 1; } else { // If we've already got an array, just append. existing.push(listener); @@ -243,8 +431,8 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { // Check for listener leak if (!existing.warned) { - m = $getMaxListeners(this); - if (m && m > 0 && existing.length > m) { + const m = $getMaxListeners(this); + if (m > 0 && existing.length > m) { existing.warned = true; if (!internalUtil) internalUtil = require('internal/util'); @@ -261,154 +449,213 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { return this; }; -EventEmitter.prototype.on = EventEmitter.prototype.addListener; +EventEmitter.prototype.addListener = EventEmitter.prototype.on; EventEmitter.prototype.once = function once(type, listener) { + var events; + var existing; + if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); - var fired = false; - - function g() { - this.removeListener(type, g); + events = this._events; + if (!events) { + events = this._events = {}; + 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, listener); - if (!fired) { - fired = true; - listener.apply(this, arguments); + // Re-assign `events` because a newListener handler could have caused + // `this._events` to be assigned to a new object + events = this._events; } + existing = events[type]; } - g.listener = listener; - this.on(type, g); + if (!existing) { + // Optimize the case of one listener. Don't need the extra array object. + existing = events[type] = { once: listener, fired: false }; + ++this._eventsCount; + } else { + if (typeof existing === 'function' || typeof existing.once === 'function') { + // Adding the second element, need to change to array. + existing = events[type] = [ + existing, + { once: listener, fired: false } + ]; + existing.onceCount = (typeof existing.once === 'function' ? 2 : 1); + } else { + // If we've already got an array, just append. + existing.push({ once: listener, fired: false }); + if (existing.onceCount === undefined) + existing.onceCount = 1; + else + ++existing.onceCount; + } + + // Check for listener leak + if (!existing.warned) { + const m = $getMaxListeners(this); + if (m > 0 && existing.length > m) { + existing.warned = true; + if (!internalUtil) + internalUtil = require('internal/util'); + + internalUtil.error('warning: possible EventEmitter memory ' + + 'leak detected. %d %s listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + existing.length, type); + console.trace(); + } + } + } return this; }; // emits a 'removeListener' event iff the listener was removed -EventEmitter.prototype.removeListener = - function removeListener(type, listener) { - var list, events, position, i; - - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); +EventEmitter.prototype.removeListener = removeListener; +function removeListener(type, listener) { + if (typeof listener !== 'function') + throw new TypeError('"listener" argument must be a function'); - events = this._events; - if (!events) - return this; - - list = events[type]; - if (!list) - return this; - - 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') { - position = -1; - - for (i = list.length; i-- > 0;) { - if (list[i] === listener || - (list[i].listener && list[i].listener === listener)) { - position = i; - break; - } + const events = this._events; + if (events === undefined) + return this; + + const list = events[type]; + if (list === undefined) + return this; + + if ((list.once || list) === listener) { + if (--this._eventsCount === 0) + this._events = {}; + else { + delete events[type]; + if (events.removeListener !== undefined) + this.emit('removeListener', type, listener); + } + } else if (typeof list !== 'function') { + var position = -1; + var isOnce = false; + var i; + const len = list.length; + + if (list.onceCount > 0) { + for (i = len; i-- > 0;) { + const curListener = list[i]; + if ((curListener.once || curListener) === listener) { + isOnce = (curListener !== listener); + position = i; + break; } - - if (position < 0) - return this; - - if (list.length === 1) { - list[0] = undefined; - if (--this._eventsCount === 0) { - this._events = {}; - return this; - } else { - delete events[type]; - } - } else { - spliceOne(list, position); + } + } else { + for (i = len; i-- > 0;) { + const curListener = list[i]; + if (curListener === listener) { + position = i; + break; } - - if (events.removeListener) - this.emit('removeListener', type, listener); } + } + if (position < 0) return this; - }; -EventEmitter.prototype.removeAllListeners = - function removeAllListeners(type) { - var listeners, events; + if (len > 2) { + if (isOnce) + --list.onceCount; + if (position === len - 1) + list.pop(); + else if (position === 0) + list.shift(); + else + spliceOne(list, position, len); + } else if (position === 0) { + events[type] = list[1]; + } else { + events[type] = list[0]; + } - events = this._events; - if (!events) - return this; - - // not listening for removeListener, no need to emit - if (!events.removeListener) { - if (arguments.length === 0) { - this._events = {}; - this._eventsCount = 0; - } else if (events[type]) { - if (--this._eventsCount === 0) - this._events = {}; - else - delete events[type]; - } - return this; - } + if (events.removeListener) + this.emit('removeListener', type, listener); + } - // emit removeListener for all listeners on all events - if (arguments.length === 0) { - var keys = Object.keys(events); - for (var i = 0, key; i < keys.length; ++i) { - key = keys[i]; - if (key === 'removeListener') continue; - this.removeAllListeners(key); - } - this.removeAllListeners('removeListener'); + return this; +} + +EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { + const events = this._events; + if (events === undefined) + return this; + + // not listening for removeListener, no need to emit + if (events.removeListener === undefined) { + if (type === undefined) { + this._events = {}; + this._eventsCount = 0; + } else if (events[type] !== undefined) { + if (--this._eventsCount === 0) this._events = {}; - this._eventsCount = 0; - return this; - } + else + delete events[type]; + } + return this; + } - listeners = events[type]; + // emit removeListener for all listeners on all events + if (type === undefined) { + const keys = Object.keys(events); + for (var i = 0; i < keys.length; ++i) { + const key = keys[i]; + if (key === 'removeListener') + continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = {}; + this._eventsCount = 0; + return this; + } - if (typeof listeners === 'function') { - this.removeListener(type, listeners); - } else if (listeners) { - // LIFO order - do { - this.removeListener(type, listeners[listeners.length - 1]); - } while (listeners[0]); + var listeners = events[type]; + if (listeners !== undefined) { + if (typeof listeners === 'function') { + this.removeListener(type, listeners); + } else if (typeof listeners.once === 'function') { + this.removeListener(type, listeners.once); + } else { + // LIFO order + for (i = listeners.length; i-- > 0;) { + const listener = listeners[i]; + if (typeof listener === 'function') + this.removeListener(type, listener); + else + this.removeListener(type, listener.once); } + } + } - return this; - }; + return this; +}; EventEmitter.prototype.listeners = function listeners(type) { - var evlistener; - var ret; - var events = this._events; - - if (!events) - ret = []; - else { - evlistener = events[type]; - if (!evlistener) - ret = []; - else if (typeof evlistener === 'function') - ret = [evlistener]; - else - ret = arrayClone(evlistener, evlistener.length); + const events = this._events; + if (events !== undefined) { + const listener = events[type]; + if (listener instanceof Array) { + return (!listener.onceCount + ? arrayClone(listener, listener.length) + : onceClone(listener)); + } else if (listener !== undefined) { + return [listener.once || listener]; + } } - - return ret; + return []; }; EventEmitter.listenerCount = function(emitter, type) { @@ -422,30 +669,37 @@ EventEmitter.listenerCount = function(emitter, type) { EventEmitter.prototype.listenerCount = listenerCount; function listenerCount(type) { const events = this._events; - - if (events) { - const evlistener = events[type]; - - if (typeof evlistener === 'function') { + if (events !== undefined) { + const listener = events[type]; + if (listener instanceof Array) + return listener.length; + else if (listener !== undefined) return 1; - } else if (evlistener) { - return evlistener.length; - } } - return 0; } // About 1.5x faster than the two-arg version of Array#splice(). -function spliceOne(list, index) { - for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) +function spliceOne(list, index, len) { + for (var i = index, k = i + 1, n = len || list.length; k < n; i += 1, k += 1) list[i] = list[k]; list.pop(); } -function arrayClone(arr, i) { - var copy = new Array(i); - while (i--) - copy[i] = arr[i]; + +function onceClone(arr) { + const len = arr.length; + const ret = new Array(len); + for (var i = 0; i < len; ++i) { + const fn = arr[i]; + ret[i] = fn.once || fn; + } + return ret; +} + +function arrayClone(arr, len) { + const copy = new Array(len); + while (len--) + copy[len] = arr[len]; return copy; } diff --git a/src/env.h b/src/env.h index c768120ab83d8b..3e0a70e6542a1a 100644 --- a/src/env.h +++ b/src/env.h @@ -153,6 +153,7 @@ namespace node { V(nsname_string, "nsname") \ V(ocsp_request_string, "OCSPRequest") \ V(offset_string, "offset") \ + V(once_string, "once") \ V(onchange_string, "onchange") \ V(onclienthello_string, "onclienthello") \ V(oncomplete_string, "oncomplete") \ diff --git a/src/node.cc b/src/node.cc index c71e35a45d3456..e72104df4dd54c 100644 --- a/src/node.cc +++ b/src/node.cc @@ -976,6 +976,9 @@ static bool DomainHasErrorHandler(const Environment* env, domain_event_listeners_o->Get(env->error_string()); if (domain_error_listeners_v->IsFunction() || + (domain_error_listeners_v->IsObject() && + domain_error_listeners_v.As()->Get(env->once_string()) + ->IsFunction()) || (domain_error_listeners_v->IsArray() && domain_error_listeners_v.As()->Length() > 0)) return true; diff --git a/test/parallel/test-event-emitter-listener-count.js b/test/parallel/test-event-emitter-listener-count.js index ebfed8b2ee33b1..832d70eaea5a21 100644 --- a/test/parallel/test-event-emitter-listener-count.js +++ b/test/parallel/test-event-emitter-listener-count.js @@ -16,3 +16,8 @@ assert.strictEqual(emitter.listenerCount('foo'), 2); assert.strictEqual(emitter.listenerCount('bar'), 0); assert.strictEqual(emitter.listenerCount('baz'), 1); assert.strictEqual(emitter.listenerCount(123), 1); + + +const emitter2 = new EventEmitter(); +emitter2.once('foo', function() {}); +assert.strictEqual(emitter2.listenerCount('foo'), 1); diff --git a/test/parallel/test-event-emitter-listeners.js b/test/parallel/test-event-emitter-listeners.js index 77c44907b62d8c..27ba0d56ccf611 100644 --- a/test/parallel/test-event-emitter-listeners.js +++ b/test/parallel/test-event-emitter-listeners.js @@ -1,32 +1,39 @@ 'use strict'; require('../common'); -var assert = require('assert'); -var events = require('events'); +const assert = require('assert'); +const events = require('events'); function listener() {} function listener2() {} -var e1 = new events.EventEmitter(); +const e1 = new events.EventEmitter(); e1.on('foo', listener); -var fooListeners = e1.listeners('foo'); +const fooListeners = e1.listeners('foo'); assert.deepEqual(e1.listeners('foo'), [listener]); e1.removeAllListeners('foo'); assert.deepEqual(e1.listeners('foo'), []); assert.deepEqual(fooListeners, [listener]); -var e2 = new events.EventEmitter(); +const e2 = new events.EventEmitter(); e2.on('foo', listener); -var e2ListenersCopy = e2.listeners('foo'); +const e2ListenersCopy = e2.listeners('foo'); assert.deepEqual(e2ListenersCopy, [listener]); assert.deepEqual(e2.listeners('foo'), [listener]); e2ListenersCopy.push(listener2); assert.deepEqual(e2.listeners('foo'), [listener]); assert.deepEqual(e2ListenersCopy, [listener, listener2]); -var e3 = new events.EventEmitter(); +const e3 = new events.EventEmitter(); e3.on('foo', listener); -var e3ListenersCopy = e3.listeners('foo'); +const e3ListenersCopy = e3.listeners('foo'); e3.on('foo', listener2); assert.deepEqual(e3.listeners('foo'), [listener, listener2]); assert.deepEqual(e3ListenersCopy, [listener]); + +const e4 = new events.EventEmitter(); +e4.once('foo', listener); +const e4ListenersCopy = e4.listeners('foo'); +e4.on('foo', listener2); +assert.deepEqual(e4.listeners('foo'), [listener, listener2]); +assert.deepEqual(e4ListenersCopy, [listener]); diff --git a/test/parallel/test-event-emitter-method-names.js b/test/parallel/test-event-emitter-method-names.js index c1e6540f0184af..a43ac74bfa89b2 100644 --- a/test/parallel/test-event-emitter-method-names.js +++ b/test/parallel/test-event-emitter-method-names.js @@ -7,7 +7,7 @@ var E = events.EventEmitter.prototype; assert.equal(E.constructor.name, 'EventEmitter'); assert.equal(E.on, E.addListener); // Same method. Object.getOwnPropertyNames(E).forEach(function(name) { - if (name === 'constructor' || name === 'on') return; + if (name === 'constructor' || name === 'addListener') return; if (typeof E[name] !== 'function') return; assert.equal(E[name].name, name); });