Skip to content

Should EmitterMixin.stopListening() off all listeners (those added through on() too)? #4947

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Reinmar opened this issue Apr 4, 2017 · 10 comments · Fixed by ckeditor/ckeditor5-utils#190
Assignees
Labels
package:utils type:bug This issue reports a buggy (incorrect) behavior. type:improvement This issue reports a possible enhancement of an existing feature.
Milestone

Comments

@Reinmar
Copy link
Member

Reinmar commented Apr 4, 2017

While working on ckeditor/ckeditor5-engine#904 @scofalik noticed that stopListening() is not removing listeners added using on().

I assume it was such a case, right @scofalik?

a.on( 'x',  callback );

a.stopListening();

If this is true, then it means that you can't easily clean all listeners that you registered. My first thought was that it makes this method useless.

However, as I found that I already explained in ckeditor/ckeditor5-design#142 (sic! :D), this.on() is to be used by a class internally. So if class X needs to listen to its own events, it can use this.on(). This means that together with the class its listeners will be gone because no more events will be fired as well.

However, again, this is a theory, because we don't enforce the rule that you can fire events only on yourself. So someone may mistakenly still fire events on an already destroyed instance of class X and the callbacks will be executed. This increases a chance of a bug.

Taken that, I'd say that:

  • either stopListening() should off() all listeners added by on(),
  • or we should always use listenTo() and remove on() (alternatively, on() should use listenTo() internally).

I wouldn't totally remove on() because its shortness is useful. Having to always do a.listenTo( a, ...) would be cumbersome. Also, we use on() mostly in tests and half of those cases where we use it source are incorrect (listenTo() should be used with a proper subject).

So, I'm either for making on() a shortcut to a.listenTo( a ) or stopListening() offing all events. In the former case we need to figure out what off() should do. In the latter, nothing changes AFAICT.

WDYT?

@oleq
Copy link
Member

oleq commented Apr 4, 2017

stopListening() offing all events.

👍

I'm really surprised it works that way ATM. I'm pretty sure outsiders will be thrice as surprised because of that and I think it's a good enough reason to rectify stopListening.

@Reinmar
Copy link
Member Author

Reinmar commented Apr 4, 2017

Actually, please guys note that making stopListening() offing all events doesn't mean that on() can't be a shorthand for a.listenTo( a ). The latter is an additional improvement to the internals and possible simplification to the model. There are only listeners added by listenTo(), all are tracked and all are removed by stopListening().

So, this is kinda additional question – do you think that this should also be done?

@oskarwrobel
Copy link
Contributor

listenTo() and once() uses on() internally.

@jodator
Copy link
Contributor

jodator commented Apr 4, 2017

I would go with anything that is more common. The stopListening() removing all events handlers looks like expected thing. We have already one "nonstandard" thing (fire vs trigger ;) )in events but we shouldn't introduce more.

@scofalik
Copy link
Contributor

scofalik commented Apr 5, 2017

I'd go even further and either remove on/off pair or listenTo/stopListening pair.

Edit: Okay, maybe not that far, using .on() is sometimes useful. But let's make it a standard that we use listenTo and stopListening and leave on for specific / corner cases.

@Reinmar
Copy link
Member Author

Reinmar commented Apr 5, 2017

Edit: Okay, maybe not that far, using .on() is sometimes useful. But let's make it a standard that we use listenTo and stopListening and leave on for specific / corner cases.

Yes, using on() is useful in e.g. tests, with short-lived objects. The same, if an object uses it on itself. So it's more about making on() behaving nicely (being easily off-able).

@scofalik
Copy link
Contributor

scofalik commented Apr 7, 2017

Okay so I see that we all agree that stopListening() should also off() events.

@Reinmar
Copy link
Member Author

Reinmar commented Apr 11, 2017

I R-ed @scofalik's PR: ckeditor/ckeditor5-utils#145 (comment)

The PR implements the simplest solution (forced off() on stopListening()), but for me it just made things messier. After looking at the code, I'm now more convinced that we need to clean up that module.

So, right now on() is the base method and listenTo() uses it internally. I think that it will be much clearer if listenTo() is the base method and on() is just this.listenTo( this, ... ). Similar change needs to be done with off() and stopListening().

@jodator jodator self-assigned this Oct 17, 2017
@jodator
Copy link
Contributor

jodator commented Oct 18, 2017

OK I've dig into this and checking how @scofalik fixed memory leaks in ckeditor/ckeditor5-utils#145 PR. Also I've refactore (in t/144-new those methods so there are two: listenTo and stopListening).

In term of memory leaks we have to remember which object called listenTo.

Event callbacks will always be stored at emiter and given that it enforces that either:
a) remember to to stop listening to some object
b) call listenTo on this object
A:

this.listenTo( someEmitter, 'foo', () => {} );

// other palce in code:
this.listenTo( someEmitter, 'bar', () => {} );

// later in code: when someEmitter has reached End Of Life
this.stopListening( someEmitter );

B:

someEmitter.listenTo( someEmitter, 'foo', () => {} );
// same as:
someEmitter.on( 'foo', () => {} );

// other palce in code:
someEmitter.listenTo( someEmitter, 'bar', () => {} );

// here nothing have to be done as log no reference to `someEmitter` exists 
// it will be garbage collected.

The most problematic part of code as I see it is in listenTo mechanism that registers on which Emitters current Observer have registered callbacks so it can dereference them with stopListening(). In other words as long as Observer will not call this.stopListening( someEmitter ) that long a reference to Emitter will be in Observer thus memory will not be freed.

@pjasiun said that from documantation POV it might be clearer when there will be two separate classes: Emitter and Observer (or other names). The latter with listenTo and stopListening methods. But still we will have the same problem of where references are stored.

@Reinmar
Copy link
Member Author

Reinmar commented Oct 18, 2017

The B option is much better. If you're using an object which is supposed to be temporary, you should pick it as the observer (so use obj.listenTo( obj, ... )). This is safer cause you don't need to remember about this.stopListening( obj ).

Reinmar referenced this issue in ckeditor/ckeditor5-utils Dec 11, 2017
Other: Aligned behaviors of `EmitterMixin` methods responsible for adding end removing listeners. Closes #144.

The `emitter.on()` now has the same behavior as `emitter.listenTo( emitter )` as well as `emitter.off()` is the same as `emitter.stopListening( emitter )`. This made `emitter.stopListening()` correctly remove all listeners added in any way to it which prevents memory leaks.
@mlewand mlewand transferred this issue from ckeditor/ckeditor5-utils Oct 9, 2019
@mlewand mlewand added this to the iteration 14 milestone Oct 9, 2019
@mlewand mlewand added status:confirmed type:bug This issue reports a buggy (incorrect) behavior. type:improvement This issue reports a possible enhancement of an existing feature. package:utils labels Oct 9, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
package:utils type:bug This issue reports a buggy (incorrect) behavior. type:improvement This issue reports a possible enhancement of an existing feature.
Projects
None yet
6 participants