Skip to content

Commit 0e7d57a

Browse files
committed
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: #1817 PR-URL: #6032 Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com> Reviewed-By: Brian White <mscdex@mscdex.net>
1 parent f85412d commit 0e7d57a

File tree

4 files changed

+174
-29
lines changed

4 files changed

+174
-29
lines changed

doc/api/events.md

+70
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,9 @@ console.log(util.inspect(server.listeners('connection')));
340340

341341
### emitter.on(eventName, listener)
342342

343+
* `eventName` {string|Symbol} The name of the event.
344+
* `listener` {Function} The callback function
345+
343346
Adds the `listener` function to the end of the listeners array for the
344347
event named `eventName`. No checks are made to see if the `listener` has
345348
already been added. Multiple calls passing the same combination of `eventName`
@@ -354,8 +357,25 @@ server.on('connection', (stream) => {
354357

355358
Returns a reference to the `EventEmitter` so calls can be chained.
356359

360+
By default, event listeners are invoked in the order they are added. The
361+
`emitter.prependListener()` method can be used as an alternative to add the
362+
event listener to the beginning of the listeners array.
363+
364+
```js
365+
const myEE = new EventEmitter();
366+
myEE.on('foo', () => console.log('a'));
367+
myEE.prependListener('foo', () => console.log('b'));
368+
myEE.emit('foo');
369+
// Prints:
370+
// b
371+
// a
372+
```
373+
357374
### emitter.once(eventName, listener)
358375

376+
* `eventName` {string|Symbol} The name of the event.
377+
* `listener` {Function} The callback function
378+
359379
Adds a **one time** `listener` function for the event named `eventName`. This
360380
listener is invoked only the next time `eventName` is triggered, after which
361381
it is removed.
@@ -368,6 +388,56 @@ server.once('connection', (stream) => {
368388

369389
Returns a reference to the `EventEmitter` so calls can be chained.
370390

391+
By default, event listeners are invoked in the order they are added. The
392+
`emitter.prependOnceListener()` method can be used as an alternative to add the
393+
event listener to the beginning of the listeners array.
394+
395+
```js
396+
const myEE = new EventEmitter();
397+
myEE.once('foo', () => console.log('a'));
398+
myEE.prependOnceListener('foo', () => console.log('b'));
399+
myEE.emit('foo');
400+
// Prints:
401+
// b
402+
// a
403+
```
404+
405+
### emitter.prependListener(eventName, listener)
406+
407+
* `eventName` {string|Symbol} The name of the event.
408+
* `listener` {Function} The callback function
409+
410+
Adds the `listener` function to the *beginning* of the listeners array for the
411+
event named `eventName`. No checks are made to see if the `listener` has
412+
already been added. Multiple calls passing the same combination of `eventName`
413+
and `listener` will result in the `listener` being added, and called, multiple
414+
times.
415+
416+
```js
417+
server.prependListener('connection', (stream) => {
418+
console.log('someone connected!');
419+
});
420+
```
421+
422+
Returns a reference to the `EventEmitter` so calls can be chained.
423+
424+
### emitter.prependOnceListener(eventName, listener)
425+
426+
* `eventName` {string|Symbol} The name of the event.
427+
* `listener` {Function} The callback function
428+
429+
Adds a **one time** `listener` function for the event named `eventName` to the
430+
*beginning* of the listeners array. This listener is invoked only the next time
431+
`eventName` is triggered, after which it is removed.
432+
433+
```js
434+
server.prependOnceListener('connection', (stream) => {
435+
console.log('Ah, we have our first user!');
436+
});
437+
```
438+
439+
Returns a reference to the `EventEmitter` so calls can be chained.
440+
371441
### emitter.removeAllListeners([eventName])
372442

373443
Removes all listeners, or those of the specified `eventName`.

lib/_stream_readable.js

+21-8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,25 @@ var StringDecoder;
1212

1313
util.inherits(Readable, Stream);
1414

15+
const hasPrependListener = typeof EE.prototype.prependListener === 'function';
16+
17+
function prependListener(emitter, event, fn) {
18+
if (hasPrependListener)
19+
return emitter.prependListener(event, fn);
20+
21+
// This is a brutally ugly hack to make sure that our error handler
22+
// is attached before any userland ones. NEVER DO THIS. This is here
23+
// only because this code needs to continue to work with older versions
24+
// of Node.js that do not include the prependListener() method. The goal
25+
// is to eventually remove this hack.
26+
if (!emitter._events || !emitter._events[event])
27+
emitter.on(event, fn);
28+
else if (Array.isArray(emitter._events[event]))
29+
emitter._events[event].unshift(fn);
30+
else
31+
emitter._events[event] = [fn, emitter._events[event]];
32+
}
33+
1534
function ReadableState(options, stream) {
1635
options = options || {};
1736

@@ -558,15 +577,9 @@ Readable.prototype.pipe = function(dest, pipeOpts) {
558577
if (EE.listenerCount(dest, 'error') === 0)
559578
dest.emit('error', er);
560579
}
561-
// This is a brutally ugly hack to make sure that our error handler
562-
// is attached before any userland ones. NEVER DO THIS.
563-
if (!dest._events || !dest._events.error)
564-
dest.on('error', onerror);
565-
else if (Array.isArray(dest._events.error))
566-
dest._events.error.unshift(onerror);
567-
else
568-
dest._events.error = [onerror, dest._events.error];
569580

581+
// Make sure our error handler is attached before userland ones.
582+
prependListener(dest, 'error', onerror);
570583

571584
// Both close and finish should trigger unpipe, but only once.
572585
function onclose() {

lib/events.js

+42-21
Original file line numberDiff line numberDiff line change
@@ -207,48 +207,53 @@ EventEmitter.prototype.emit = function emit(type) {
207207
return true;
208208
};
209209

210-
EventEmitter.prototype.addListener = function addListener(type, listener) {
210+
function _addListener(target, type, listener, prepend) {
211211
var m;
212212
var events;
213213
var existing;
214214

215215
if (typeof listener !== 'function')
216216
throw new TypeError('"listener" argument must be a function');
217217

218-
events = this._events;
218+
events = target._events;
219219
if (!events) {
220-
events = this._events = new EventHandlers();
221-
this._eventsCount = 0;
220+
events = target._events = new EventHandlers();
221+
target._eventsCount = 0;
222222
} else {
223223
// To avoid recursion in the case that type === "newListener"! Before
224224
// adding it to the listeners, first emit "newListener".
225225
if (events.newListener) {
226-
this.emit('newListener', type,
227-
listener.listener ? listener.listener : listener);
226+
target.emit('newListener', type,
227+
listener.listener ? listener.listener : listener);
228228

229229
// Re-assign `events` because a newListener handler could have caused the
230230
// this._events to be assigned to a new object
231-
events = this._events;
231+
events = target._events;
232232
}
233233
existing = events[type];
234234
}
235235

236236
if (!existing) {
237237
// Optimize the case of one listener. Don't need the extra array object.
238238
existing = events[type] = listener;
239-
++this._eventsCount;
239+
++target._eventsCount;
240240
} else {
241241
if (typeof existing === 'function') {
242242
// Adding the second element, need to change to array.
243-
existing = events[type] = [existing, listener];
243+
existing = events[type] = prepend ? [listener, existing] :
244+
[existing, listener];
244245
} else {
245246
// If we've already got an array, just append.
246-
existing.push(listener);
247+
if (prepend) {
248+
existing.unshift(listener);
249+
} else {
250+
existing.push(listener);
251+
}
247252
}
248253

249254
// Check for listener leak
250255
if (!existing.warned) {
251-
m = $getMaxListeners(this);
256+
m = $getMaxListeners(target);
252257
if (m && m > 0 && existing.length > m) {
253258
existing.warned = true;
254259
process.emitWarning('Possible EventEmitter memory leak detected. ' +
@@ -258,32 +263,48 @@ EventEmitter.prototype.addListener = function addListener(type, listener) {
258263
}
259264
}
260265

261-
return this;
266+
return target;
267+
}
268+
269+
EventEmitter.prototype.addListener = function addListener(type, listener) {
270+
return _addListener(this, type, listener, false);
262271
};
263272

264273
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
265274

266-
EventEmitter.prototype.once = function once(type, listener) {
267-
if (typeof listener !== 'function')
268-
throw new TypeError('"listener" argument must be a function');
275+
EventEmitter.prototype.prependListener =
276+
function prependListener(type, listener) {
277+
return _addListener(this, type, listener, true);
278+
};
269279

280+
function _onceWrap(target, type, listener) {
270281
var fired = false;
271-
272282
function g() {
273-
this.removeListener(type, g);
274-
283+
target.removeListener(type, g);
275284
if (!fired) {
276285
fired = true;
277-
listener.apply(this, arguments);
286+
listener.apply(target, arguments);
278287
}
279288
}
280-
281289
g.listener = listener;
282-
this.on(type, g);
290+
return g;
291+
}
283292

293+
EventEmitter.prototype.once = function once(type, listener) {
294+
if (typeof listener !== 'function')
295+
throw new TypeError('"listener" argument must be a function');
296+
this.on(type, _onceWrap(this, type, listener));
284297
return this;
285298
};
286299

300+
EventEmitter.prototype.prependOnceListener =
301+
function prependOnceListener(type, listener) {
302+
if (typeof listener !== 'function')
303+
throw new TypeError('"listener" argument must be a function');
304+
this.prependListener(type, _onceWrap(this, type, listener));
305+
return this;
306+
};
307+
287308
// emits a 'removeListener' event iff the listener was removed
288309
EventEmitter.prototype.removeListener =
289310
function removeListener(type, listener) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const EventEmitter = require('events');
5+
const assert = require('assert');
6+
7+
const myEE = new EventEmitter();
8+
var m = 0;
9+
// This one comes last.
10+
myEE.on('foo', common.mustCall(() => assert.equal(m, 2)));
11+
12+
// This one comes second.
13+
myEE.prependListener('foo', common.mustCall(() => assert.equal(m++, 1)));
14+
15+
// This one comes first.
16+
myEE.prependOnceListener('foo', common.mustCall(() => assert.equal(m++, 0)));
17+
18+
myEE.emit('foo');
19+
20+
21+
// Test fallback if prependListener is undefined.
22+
const stream = require('stream');
23+
const util = require('util');
24+
25+
delete EventEmitter.prototype.prependListener;
26+
27+
function Writable() {
28+
this.writable = true;
29+
stream.Stream.call(this);
30+
}
31+
util.inherits(Writable, stream.Stream);
32+
33+
function Readable() {
34+
this.readable = true;
35+
stream.Stream.call(this);
36+
}
37+
util.inherits(Readable, stream.Stream);
38+
39+
const w = new Writable();
40+
const r = new Readable();
41+
r.pipe(w);

0 commit comments

Comments
 (0)