Node: emitter.stopImmediatePropagation()

Created on 13 Nov 2019  路  6Comments  路  Source: nodejs/node

Reference:
https://developer.mozilla.org/en-US/docs/Web/API/Event/stopImmediatePropagation

Seems we can implement this function. Any idea?

Here is a stand alone simplified EventEmitter written in ES6 class style, which shows how stopImmediatePropagation works.
```js
class EventEmitter {

static _maxListeners = 10;

static get maxListeners() {
    return this._maxListeners;
}

static set maxListeners(num) {
    if (typeof num === 'number' && num > 0) {
        return this._maxListeners = num;
    }
}

#events = Object.create(null);

#maxListeners = undefined;

emit(event, ...args) {
    const listener = this.#events[event];
    if (listener && this.#stopImmediatePropagation === false) {
        if (typeof listener === 'function') {
            args.length ? listener.apply(this, args) : listener.call(this);
        } else { 
            if (args.length) {
                for (let each of listener.slice()) {
                    each.apply(this, args);
                    if (this.#stopImmediatePropagation === true) {
                        this.#stopImmediatePropagation = false;
                        break;
                    }
                }
            } else {
                for (let each of listener.slice()) {
                    each.call(this);
                    if (this.#stopImmediatePropagation === true) {
                        this.#stopImmediatePropagation = false;
                        break;
                    }
                }
            }
        }
        return true;
    }
    if (event === 'error') {
        handleError(args[0]);
    }
    return false;
}

on(event, listener) {
    return addListener(this, this.#events, event, listener, false);
}

#stopImmediatePropagation = false;

stopImmediatePropagation(){
    this.#stopImmediatePropagation =  true;
}

get addListener() {
    return this.on;
}

off(event, listener) {
    removeListener(this.#events, event, listener);
    return this;
}

get removeListener() {
    return this.off;
}

get maxListeners() {
    return this.#maxListeners || this.constructor.maxListeners;
}

set maxListeners(num) {
    if (typeof num === 'number' && num > 0)
        return this.#maxListeners = num;
}

eventNames() {
    return Reflect.ownKeys(this.#events);
}

listenerCount(event) {
    const listener = this.#events[event];
    if (listener) {
        return (typeof listener === 'function') ? 1 : listener.length;
    }
    return 0;
}

listeners(event) {
    const listener = this.#events[event];
    if (listener){
        return (typeof listener === 'function') ? listener : listener.slice();
    }
    return [];
}

prependListener(event, listener) {
    return addListener(this, this.#events, event, listener, true);
}

await(event) {
    var _resolve, _reject; 
    return new Promise(
        (resolve, reject) => {
            _resolve = resolve;
            _reject = reject;
            this.on(event, resolve);
            if (event !== 'error') {
                this.on('error', reject);
            }
        }
    ).finally(
        () => {
            this.off(event, _resolve);
            if (event !== 'error') {
                this.off('error', _reject);
            }
        }
    );
}

}

module.exports = EventEmitter;

function addListener(emitter, events, event, listener, prepend) {
if (event && typeof listener === 'function') {
var existing = events[event];
if (existing) {
if (typeof existing === 'function') {
existing = events[event] = prepend ? [listener, existing] : [existing, listener];
} else {
prepend ? existing.unshift(listener) : existing.push(listener);
}
if (existing.length > emitter.maxListeners) {
console.log('Too many event listeners added. Set maxListeners to increase limit.');
}
} else {
events[event] = listener;
}
}
return emitter;
}

function removeListener(events, event, listener) {
const existing = events[event];
if (existing) {
if (listener === undefined || existing === listener) {
delete events[event];
} else {
let index = existing.lastIndexOf(listener),
len = existing.length;
if (index >= 0) {
if (len-- > 2) {
if (index > 0) {
while (index < len) {
existing[index++] = existing[index];
}
existing.pop();
} else {
existing.shift();
}
} else {
events[event] = existing[1 - index];
}
}
}
}
}

function handleError(error) {
if (error instanceof Error) {
throw error;
}
throw error ? new Error(error) : new Error();
}

function f(){
console.log('f');
}

function g(){
console.log('g');
}

function br(){
console.log('br');
this.stopImmediatePropagation();
}

var e = new EventEmitter;

e.on('event', f)
e.on('event', br);
e.on('event', g);
e.emit('event');

events feature request

Most helpful comment

I don't think we should implement this. Namely our events don't have a concept of propagation and our emitters don't chain.

DOM EventTarget is not like our EventEmitter in that events don't 'travel down then up the element tree'.

So thanks for the suggestion but I don't think this is something we should do at the moment.

All 6 comments

The event.stopImmediatePropagation() function is an Event API, and calling it prevents propagation of a single event only. Node.js does not have the Event class. Your implementation appears to implement stopImmediatePropagation on the EventEmitter class instead, and it appears to completely block all events. Is that correct? And if so, is that what you intended?

Modified.
It blocks all events after stopImmediatedPropagation() called within the event listener. If stopImmediatedPropagation() is called at the end of the event listener, there is absolutely no problem.

emit(event, ...args) {
    const listener = this.#events[event];
    if (listener && this.#stopImmediatePropagation === false) {
        if (typeof listener === 'function') {
            args.length ? listener.apply(this, args) : listener.call(this);
            this.#stopImmediatePropagation = false;
        } else { 
            if (args.length) {
                for (let each of listener.slice()) {
                    each.apply(this, args);
                    if (this.#stopImmediatePropagation === true) {
                        this.#stopImmediatePropagation = false;
                        break;
                    }
                }
            } else {
                for (let each of listener.slice()) {
                    each.call(this);
                    if (this.#stopImmediatePropagation === true) {
                        this.#stopImmediatePropagation = false;
                        break;
                    }
                }
            }
        }
        return true;
    }
    if (event === 'error') {
        handleError(args[0]);
    }
    return false;
}

It is still not compatible to the browser Event API, so I see little reason to try to make it look as if it was compatible. If people really need this behavior, they can create their own event classes and pass them to emit.

cc @nodejs/events

I don't think we should implement this. Namely our events don't have a concept of propagation and our emitters don't chain.

DOM EventTarget is not like our EventEmitter in that events don't 'travel down then up the element tree'.

So thanks for the suggestion but I don't think this is something we should do at the moment.

Modified again.
This version would not block any other event.

emit(event, ...args) {
    const listener = this.#events[event],
        previousImmediatePropagationStatus = this.#stopImmediatePropagation;
    this.#stopImmediatePropagation = false;
    if (listener) {
        if (typeof listener === 'function') {
            args.length ? listener.apply(this, args) : listener.call(this);
        } else {
            if (args.length) {
                for (let each of listener.slice()) {
                    each.apply(this, args);
                    if (this.#stopImmediatePropagation === true) {
                        break;
                    }
                }
            } else {
                for (let each of listener.slice()) {
                    each.call(this);
                    if (this.#stopImmediatePropagation === true) {
                        break;
                    }
                }
            }
        }
        this.#stopImmediatePropagation = previousImmediatePropagationStatus;
        return true;
    }
    if (event === 'error') {
        handleError(args[0]);
    }
    return false;
}

Thank you, but as @benjamingr and I explained, we don't think this fits into our existing API very well. stopImmediatePropagation makes sense in the DOM Event model, but that is very different from events in Node.js.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dfahlander picture dfahlander  路  3Comments

seishun picture seishun  路  3Comments

mcollina picture mcollina  路  3Comments

fanjunzhi picture fanjunzhi  路  3Comments

cong88 picture cong88  路  3Comments