From d335209358811cf070ba15ae359a010bcdd189e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrg=C3=BCn=20Day=C4=B1o=C4=9Flu?= Date: Sat, 14 Mar 2026 23:04:20 +0100 Subject: [PATCH] events: avoid cloning listeners array on every emit --- lib/events.js | 63 +++++++++++++------ .../test-event-emitter-modify-in-emit.js | 18 ++++++ 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/lib/events.js b/lib/events.js index c065e43d6d2565..c6fc170d590aea 100644 --- a/lib/events.js +++ b/lib/events.js @@ -87,6 +87,7 @@ const { addAbortListener } = require('internal/events/abort_listener'); const kCapture = Symbol('kCapture'); const kErrorMonitor = Symbol('events.errorMonitor'); const kShapeMode = Symbol('shapeMode'); +const kEmitting = Symbol('events.emitting'); const kMaxEventTargetListeners = Symbol('events.maxEventTargetListeners'); const kMaxEventTargetListenersWarned = Symbol('events.maxEventTargetListenersWarned'); @@ -514,19 +515,22 @@ EventEmitter.prototype.emit = function emit(type, ...args) { addCatch(this, result, type, args); } } else { - const len = handler.length; - const listeners = arrayClone(handler); - for (let i = 0; i < len; ++i) { - const result = ReflectApply(listeners[i], this, args); - - // We check if result is undefined first because that - // is the most common case so we do not pay any perf - // penalty. - // This code is duplicated because extracting it away - // would make it non-inlineable. - if (result !== undefined && result !== null) { - addCatch(this, result, type, args); + handler[kEmitting]++; + try { + for (let i = 0; i < handler.length; ++i) { + const result = ReflectApply(handler[i], this, args); + + // We check if result is undefined first because that + // is the most common case so we do not pay any perf + // penalty. + // This code is duplicated because extracting it away + // would make it non-inlineable. + if (result !== undefined && result !== null) { + addCatch(this, result, type, args); + } } + } finally { + handler[kEmitting]--; } } @@ -565,13 +569,17 @@ function _addListener(target, type, listener, prepend) { } else { if (typeof existing === 'function') { // Adding the second element, need to change to array. - existing = events[type] = - prepend ? [listener, existing] : [existing, listener]; + existing = prepend ? [listener, existing] : [existing, listener]; + existing[kEmitting] = 0; + events[type] = existing; // If we've already got an array, just append. - } else if (prepend) { - existing.unshift(listener); } else { - existing.push(listener); + existing = ensureMutableListenerArray(events, type, existing); + if (prepend) { + existing.unshift(listener); + } else { + existing.push(listener); + } } // Check for listener leak @@ -674,7 +682,7 @@ EventEmitter.prototype.removeListener = if (events === undefined) return this; - const list = events[type]; + let list = events[type]; if (list === undefined) return this; @@ -692,6 +700,7 @@ EventEmitter.prototype.removeListener = if (events.removeListener !== undefined) this.emit('removeListener', type, list.listener || listener); } else if (typeof list !== 'function') { + list = ensureMutableListenerArray(events, type, list); let position = -1; for (let i = list.length - 1; i >= 0; i--) { @@ -875,6 +884,24 @@ function arrayClone(arr) { return ArrayPrototypeSlice(arr); } +function cloneEventListenerArray(arr) { + const copy = arrayClone(arr); + copy[kEmitting] = 0; + if (arr.warned) { + copy.warned = true; + } + return copy; +} + +function ensureMutableListenerArray(events, type, handler) { + if (handler[kEmitting] > 0) { + const copy = cloneEventListenerArray(handler); + events[type] = copy; + return copy; + } + return handler; +} + function unwrapListeners(arr) { const ret = arrayClone(arr); for (let i = 0; i < ret.length; ++i) { diff --git a/test/parallel/test-event-emitter-modify-in-emit.js b/test/parallel/test-event-emitter-modify-in-emit.js index 995fa01d11a1a8..fecabca9def3e8 100644 --- a/test/parallel/test-event-emitter-modify-in-emit.js +++ b/test/parallel/test-event-emitter-modify-in-emit.js @@ -78,3 +78,21 @@ assert.strictEqual(e.listeners('foo').length, 2); e.emit('foo'); assert.deepStrictEqual(['callback2', 'callback3'], callbacks_called); assert.strictEqual(e.listeners('foo').length, 0); + +// Verify that removing all callbacks while in emit allows the current emit to +// propagate to all listeners. +callbacks_called = []; + +function callback4() { + callbacks_called.push('callback4'); + e.removeAllListeners('foo'); +} + +e.on('foo', callback4); +e.on('foo', callback2); +e.on('foo', callback3); +assert.strictEqual(e.listeners('foo').length, 3); +e.emit('foo'); +assert.deepStrictEqual(['callback4', 'callback2', 'callback3'], + callbacks_called); +assert.strictEqual(e.listeners('foo').length, 0);