events: add prependListener() and prependOnceListener()
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: Сковорода Никита Андреевич <chalkerx@gmail.com> Reviewed-By: Brian White <mscdex@mscdex.net>
This commit is contained in:
parent
f85412d49b
commit
0e7d57af35
@ -340,6 +340,9 @@ console.log(util.inspect(server.listeners('connection')));
|
|||||||
|
|
||||||
### emitter.on(eventName, listener)
|
### 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
|
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
|
event named `eventName`. No checks are made to see if the `listener` has
|
||||||
already been added. Multiple calls passing the same combination of `eventName`
|
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.
|
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)
|
### 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
|
Adds a **one time** `listener` function for the event named `eventName`. This
|
||||||
listener is invoked only the next time `eventName` is triggered, after which
|
listener is invoked only the next time `eventName` is triggered, after which
|
||||||
it is removed.
|
it is removed.
|
||||||
@ -368,6 +388,56 @@ server.once('connection', (stream) => {
|
|||||||
|
|
||||||
Returns a reference to the `EventEmitter` so calls can be chained.
|
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])
|
### emitter.removeAllListeners([eventName])
|
||||||
|
|
||||||
Removes all listeners, or those of the specified `eventName`.
|
Removes all listeners, or those of the specified `eventName`.
|
||||||
|
@ -12,6 +12,25 @@ var StringDecoder;
|
|||||||
|
|
||||||
util.inherits(Readable, Stream);
|
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) {
|
function ReadableState(options, stream) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
@ -558,15 +577,9 @@ Readable.prototype.pipe = function(dest, pipeOpts) {
|
|||||||
if (EE.listenerCount(dest, 'error') === 0)
|
if (EE.listenerCount(dest, 'error') === 0)
|
||||||
dest.emit('error', er);
|
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.
|
// Both close and finish should trigger unpipe, but only once.
|
||||||
function onclose() {
|
function onclose() {
|
||||||
|
@ -207,7 +207,7 @@ EventEmitter.prototype.emit = function emit(type) {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
EventEmitter.prototype.addListener = function addListener(type, listener) {
|
function _addListener(target, type, listener, prepend) {
|
||||||
var m;
|
var m;
|
||||||
var events;
|
var events;
|
||||||
var existing;
|
var existing;
|
||||||
@ -215,20 +215,20 @@ EventEmitter.prototype.addListener = function addListener(type, listener) {
|
|||||||
if (typeof listener !== 'function')
|
if (typeof listener !== 'function')
|
||||||
throw new TypeError('"listener" argument must be a function');
|
throw new TypeError('"listener" argument must be a function');
|
||||||
|
|
||||||
events = this._events;
|
events = target._events;
|
||||||
if (!events) {
|
if (!events) {
|
||||||
events = this._events = new EventHandlers();
|
events = target._events = new EventHandlers();
|
||||||
this._eventsCount = 0;
|
target._eventsCount = 0;
|
||||||
} else {
|
} 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,
|
target.emit('newListener', type,
|
||||||
listener.listener ? listener.listener : listener);
|
listener.listener ? listener.listener : listener);
|
||||||
|
|
||||||
// Re-assign `events` because a newListener handler could have caused the
|
// Re-assign `events` because a newListener handler could have caused the
|
||||||
// this._events to be assigned to a new object
|
// this._events to be assigned to a new object
|
||||||
events = this._events;
|
events = target._events;
|
||||||
}
|
}
|
||||||
existing = events[type];
|
existing = events[type];
|
||||||
}
|
}
|
||||||
@ -236,19 +236,24 @@ EventEmitter.prototype.addListener = function addListener(type, listener) {
|
|||||||
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;
|
||||||
++this._eventsCount;
|
++target._eventsCount;
|
||||||
} else {
|
} else {
|
||||||
if (typeof existing === 'function') {
|
if (typeof existing === 'function') {
|
||||||
// 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] = prepend ? [listener, existing] :
|
||||||
|
[existing, listener];
|
||||||
} else {
|
} else {
|
||||||
// If we've already got an array, just append.
|
// 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
|
// Check for listener leak
|
||||||
if (!existing.warned) {
|
if (!existing.warned) {
|
||||||
m = $getMaxListeners(this);
|
m = $getMaxListeners(target);
|
||||||
if (m && m > 0 && existing.length > m) {
|
if (m && m > 0 && existing.length > m) {
|
||||||
existing.warned = true;
|
existing.warned = true;
|
||||||
process.emitWarning('Possible EventEmitter memory leak detected. ' +
|
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.on = EventEmitter.prototype.addListener;
|
||||||
|
|
||||||
|
EventEmitter.prototype.prependListener =
|
||||||
|
function prependListener(type, listener) {
|
||||||
|
return _addListener(this, type, listener, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
function _onceWrap(target, type, listener) {
|
||||||
|
var fired = false;
|
||||||
|
function g() {
|
||||||
|
target.removeListener(type, g);
|
||||||
|
if (!fired) {
|
||||||
|
fired = true;
|
||||||
|
listener.apply(target, arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.listener = listener;
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
EventEmitter.prototype.once = function once(type, listener) {
|
EventEmitter.prototype.once = function once(type, listener) {
|
||||||
if (typeof listener !== 'function')
|
if (typeof listener !== 'function')
|
||||||
throw new TypeError('"listener" argument must be a function');
|
throw new TypeError('"listener" argument must be a function');
|
||||||
|
this.on(type, _onceWrap(this, type, listener));
|
||||||
var fired = false;
|
|
||||||
|
|
||||||
function g() {
|
|
||||||
this.removeListener(type, g);
|
|
||||||
|
|
||||||
if (!fired) {
|
|
||||||
fired = true;
|
|
||||||
listener.apply(this, arguments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
g.listener = listener;
|
|
||||||
this.on(type, g);
|
|
||||||
|
|
||||||
return this;
|
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
|
// emits a 'removeListener' event iff the listener was removed
|
||||||
EventEmitter.prototype.removeListener =
|
EventEmitter.prototype.removeListener =
|
||||||
function removeListener(type, listener) {
|
function removeListener(type, listener) {
|
||||||
|
41
test/parallel/test-event-emitter-prepend.js
Normal file
41
test/parallel/test-event-emitter-prepend.js
Normal file
@ -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);
|
Loading…
x
Reference in New Issue
Block a user