From 0e7d57af3573b4dcba81217bba2f041dbdc173dc Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 3 Apr 2016 19:55:22 -0700 Subject: [PATCH] events: add prependListener() and prependOnceListener() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A handful of modules (including readable-streams) make inappropriate use of the internal _events property. One such use is to prepend an event listener to the front of the array of listeners. This adds EE.prototype.prependListener() and EE.prototype.prependOnceListener() methods to add handlers to the *front* of the listener array. Doc update and test case is included. Fixes: https://github.com/nodejs/node/issues/1817 PR-URL: https://github.com/nodejs/node/pull/6032 Reviewed-By: Сковорода Никита Андреевич Reviewed-By: Brian White --- doc/api/events.md | 70 +++++++++++++++++++++ lib/_stream_readable.js | 29 ++++++--- lib/events.js | 63 ++++++++++++------- test/parallel/test-event-emitter-prepend.js | 41 ++++++++++++ 4 files changed, 174 insertions(+), 29 deletions(-) create mode 100644 test/parallel/test-event-emitter-prepend.js diff --git a/doc/api/events.md b/doc/api/events.md index 393a69ee02a8eb..501a94b79d7dc7 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -340,6 +340,9 @@ console.log(util.inspect(server.listeners('connection'))); ### emitter.on(eventName, listener) +* `eventName` {string|Symbol} The name of the event. +* `listener` {Function} The callback function + Adds the `listener` function to the end of the listeners array for the event named `eventName`. No checks are made to see if the `listener` has already been added. Multiple calls passing the same combination of `eventName` @@ -354,8 +357,25 @@ server.on('connection', (stream) => { Returns a reference to the `EventEmitter` so calls can be chained. +By default, event listeners are invoked in the order they are added. The +`emitter.prependListener()` method can be used as an alternative to add the +event listener to the beginning of the listeners array. + +```js +const myEE = new EventEmitter(); +myEE.on('foo', () => console.log('a')); +myEE.prependListener('foo', () => console.log('b')); +myEE.emit('foo'); + // Prints: + // b + // a +``` + ### emitter.once(eventName, listener) +* `eventName` {string|Symbol} The name of the event. +* `listener` {Function} The callback function + Adds a **one time** `listener` function for the event named `eventName`. This listener is invoked only the next time `eventName` is triggered, after which it is removed. @@ -368,6 +388,56 @@ server.once('connection', (stream) => { Returns a reference to the `EventEmitter` so calls can be chained. +By default, event listeners are invoked in the order they are added. The +`emitter.prependOnceListener()` method can be used as an alternative to add the +event listener to the beginning of the listeners array. + +```js +const myEE = new EventEmitter(); +myEE.once('foo', () => console.log('a')); +myEE.prependOnceListener('foo', () => console.log('b')); +myEE.emit('foo'); + // Prints: + // b + // a +``` + +### emitter.prependListener(eventName, listener) + +* `eventName` {string|Symbol} The name of the event. +* `listener` {Function} The callback function + +Adds the `listener` function to the *beginning* of the listeners array for the +event named `eventName`. No checks are made to see if the `listener` has +already been added. Multiple calls passing the same combination of `eventName` +and `listener` will result in the `listener` being added, and called, multiple +times. + +```js +server.prependListener('connection', (stream) => { + console.log('someone connected!'); +}); +``` + +Returns a reference to the `EventEmitter` so calls can be chained. + +### emitter.prependOnceListener(eventName, listener) + +* `eventName` {string|Symbol} The name of the event. +* `listener` {Function} The callback function + +Adds a **one time** `listener` function for the event named `eventName` to the +*beginning* of the listeners array. This listener is invoked only the next time +`eventName` is triggered, after which it is removed. + +```js +server.prependOnceListener('connection', (stream) => { + console.log('Ah, we have our first user!'); +}); +``` + +Returns a reference to the `EventEmitter` so calls can be chained. + ### emitter.removeAllListeners([eventName]) Removes all listeners, or those of the specified `eventName`. diff --git a/lib/_stream_readable.js b/lib/_stream_readable.js index 275691cbb2e805..afe3b3baf065c2 100644 --- a/lib/_stream_readable.js +++ b/lib/_stream_readable.js @@ -12,6 +12,25 @@ var StringDecoder; util.inherits(Readable, Stream); +const hasPrependListener = typeof EE.prototype.prependListener === 'function'; + +function prependListener(emitter, event, fn) { + if (hasPrependListener) + return emitter.prependListener(event, fn); + + // This is a brutally ugly hack to make sure that our error handler + // is attached before any userland ones. NEVER DO THIS. This is here + // only because this code needs to continue to work with older versions + // of Node.js that do not include the prependListener() method. The goal + // is to eventually remove this hack. + if (!emitter._events || !emitter._events[event]) + emitter.on(event, fn); + else if (Array.isArray(emitter._events[event])) + emitter._events[event].unshift(fn); + else + emitter._events[event] = [fn, emitter._events[event]]; +} + function ReadableState(options, stream) { options = options || {}; @@ -558,15 +577,9 @@ Readable.prototype.pipe = function(dest, pipeOpts) { if (EE.listenerCount(dest, 'error') === 0) dest.emit('error', er); } - // This is a brutally ugly hack to make sure that our error handler - // 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]; + // Make sure our error handler is attached before userland ones. + prependListener(dest, 'error', onerror); // Both close and finish should trigger unpipe, but only once. function onclose() { diff --git a/lib/events.js b/lib/events.js index 36eb7835e40ec0..04d13d4386a3d2 100644 --- a/lib/events.js +++ b/lib/events.js @@ -207,7 +207,7 @@ EventEmitter.prototype.emit = function emit(type) { return true; }; -EventEmitter.prototype.addListener = function addListener(type, listener) { +function _addListener(target, type, listener, prepend) { var m; var events; var existing; @@ -215,20 +215,20 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); - events = this._events; + events = target._events; if (!events) { - events = this._events = new EventHandlers(); - this._eventsCount = 0; + events = target._events = new EventHandlers(); + target._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.listener ? listener.listener : listener); + target.emit('newListener', type, + 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; + events = target._events; } existing = events[type]; } @@ -236,19 +236,24 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { if (!existing) { // Optimize the case of one listener. Don't need the extra array object. existing = events[type] = listener; - ++this._eventsCount; + ++target._eventsCount; } else { if (typeof existing === 'function') { // Adding the second element, need to change to array. - existing = events[type] = [existing, listener]; + existing = events[type] = prepend ? [listener, existing] : + [existing, listener]; } else { // If we've already got an array, just append. - existing.push(listener); + if (prepend) { + existing.unshift(listener); + } else { + existing.push(listener); + } } // Check for listener leak if (!existing.warned) { - m = $getMaxListeners(this); + m = $getMaxListeners(target); if (m && m > 0 && existing.length > m) { existing.warned = true; process.emitWarning('Possible EventEmitter memory leak detected. ' + @@ -258,32 +263,48 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { } } - return this; + return target; +} + +EventEmitter.prototype.addListener = function addListener(type, listener) { + return _addListener(this, type, listener, false); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; -EventEmitter.prototype.once = function once(type, listener) { - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); +EventEmitter.prototype.prependListener = + function prependListener(type, listener) { + return _addListener(this, type, listener, true); + }; +function _onceWrap(target, type, listener) { var fired = false; - function g() { - this.removeListener(type, g); - + target.removeListener(type, g); if (!fired) { fired = true; - listener.apply(this, arguments); + listener.apply(target, arguments); } } - g.listener = listener; - this.on(type, g); + return g; +} +EventEmitter.prototype.once = function once(type, listener) { + if (typeof listener !== 'function') + throw new TypeError('"listener" argument must be a function'); + this.on(type, _onceWrap(this, type, listener)); return this; }; +EventEmitter.prototype.prependOnceListener = + function prependOnceListener(type, listener) { + if (typeof listener !== 'function') + throw new TypeError('"listener" argument must be a function'); + this.prependListener(type, _onceWrap(this, type, listener)); + return this; + }; + // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function removeListener(type, listener) { diff --git a/test/parallel/test-event-emitter-prepend.js b/test/parallel/test-event-emitter-prepend.js new file mode 100644 index 00000000000000..6be63a2616d332 --- /dev/null +++ b/test/parallel/test-event-emitter-prepend.js @@ -0,0 +1,41 @@ +'use strict'; + +const common = require('../common'); +const EventEmitter = require('events'); +const assert = require('assert'); + +const myEE = new EventEmitter(); +var m = 0; +// This one comes last. +myEE.on('foo', common.mustCall(() => assert.equal(m, 2))); + +// This one comes second. +myEE.prependListener('foo', common.mustCall(() => assert.equal(m++, 1))); + +// This one comes first. +myEE.prependOnceListener('foo', common.mustCall(() => assert.equal(m++, 0))); + +myEE.emit('foo'); + + +// Test fallback if prependListener is undefined. +const stream = require('stream'); +const util = require('util'); + +delete EventEmitter.prototype.prependListener; + +function Writable() { + this.writable = true; + stream.Stream.call(this); +} +util.inherits(Writable, stream.Stream); + +function Readable() { + this.readable = true; + stream.Stream.call(this); +} +util.inherits(Readable, stream.Stream); + +const w = new Writable(); +const r = new Readable(); +r.pipe(w);