Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit 460d7f4

Browse files
authored
Merge pull request #190 from ckeditor/t/144-new
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.
2 parents 84ccda2 + cd21a75 commit 460d7f4

File tree

4 files changed

+371
-104
lines changed

4 files changed

+371
-104
lines changed

src/dom/emittermixin.js

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const DomEmitterMixin = extend( {}, EmitterMixin, {
4040
* Registers a callback function to be executed when an event is fired in a specific Emitter or DOM Node.
4141
* It is backwards compatible with {@link module:utils/emittermixin~EmitterMixin#listenTo}.
4242
*
43+
* @method module:utils/dom/emittermixin~EmitterMixin#listenTo
4344
* @param {module:utils/emittermixin~Emitter|Node} emitter The object that fires the event.
4445
* @param {String} event The name of the event.
4546
* @param {Function} callback The function to be called on event.
@@ -49,20 +50,20 @@ const DomEmitterMixin = extend( {}, EmitterMixin, {
4950
* order they were added.
5051
* @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered
5152
* listener before being dispatched to any EventTarget beneath it in the DOM tree.
52-
*
53-
* @method module:utils/dom/emittermixin~EmitterMixin#listenTo
5453
*/
55-
listenTo( ...args ) {
56-
const emitter = args[ 0 ];
57-
54+
listenTo( emitter, ...rest ) {
5855
// Check if emitter is an instance of DOM Node. If so, replace the argument with
5956
// corresponding ProxyEmitter (or create one if not existing).
6057
if ( isDomNode( emitter ) || isWindow( emitter ) ) {
61-
args[ 0 ] = this._getProxyEmitter( emitter ) || new ProxyEmitter( emitter );
58+
const proxy = this._getProxyEmitter( emitter ) || new ProxyEmitter( emitter );
59+
60+
proxy.attach( ...rest );
61+
62+
emitter = proxy;
6263
}
6364

6465
// Execute parent class method with Emitter (or ProxyEmitter) instance.
65-
EmitterMixin.listenTo.apply( this, args );
66+
EmitterMixin.listenTo.call( this, emitter, ...rest );
6667
},
6768

6869
/**
@@ -74,17 +75,14 @@ const DomEmitterMixin = extend( {}, EmitterMixin, {
7475
* * To stop listening to all events fired by a specific object.
7576
* * To stop listening to all events fired by all object.
7677
*
78+
* @method module:utils/dom/emittermixin~EmitterMixin#stopListening
7779
* @param {module:utils/emittermixin~Emitter|Node} [emitter] The object to stop listening to. If omitted, stops it for all objects.
7880
* @param {String} [event] (Requires the `emitter`) The name of the event to stop listening to. If omitted, stops it
7981
* for all events from `emitter`.
8082
* @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given
8183
* `event`.
82-
*
83-
* @method module:utils/dom/emittermixin~EmitterMixin#stopListening
8484
*/
85-
stopListening( ...args ) {
86-
const emitter = args[ 0 ];
87-
85+
stopListening( emitter, event, callback ) {
8886
// Check if emitter is an instance of DOM Node. If so, replace the argument with corresponding ProxyEmitter.
8987
if ( isDomNode( emitter ) || isWindow( emitter ) ) {
9088
const proxy = this._getProxyEmitter( emitter );
@@ -94,11 +92,15 @@ const DomEmitterMixin = extend( {}, EmitterMixin, {
9492
return;
9593
}
9694

97-
args[ 0 ] = proxy;
95+
emitter = proxy;
9896
}
9997

10098
// Execute parent class method with Emitter (or ProxyEmitter) instance.
101-
EmitterMixin.stopListening.apply( this, args );
99+
EmitterMixin.stopListening.call( this, emitter, event, callback );
100+
101+
if ( emitter instanceof ProxyEmitter ) {
102+
emitter.detach( event );
103+
}
102104
},
103105

104106
/**
@@ -173,21 +175,14 @@ extend( ProxyEmitter.prototype, EmitterMixin, {
173175
* It attaches a native DOM listener to the DOM Node. When fired,
174176
* a corresponding Emitter event will also fire with DOM Event object as an argument.
175177
*
178+
* @method module:utils/dom/emittermixin~ProxyEmitter#attach
176179
* @param {String} event The name of the event.
177180
* @param {Function} callback The function to be called on event.
178181
* @param {Object} [options={}] Additional options.
179-
* @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
180-
* the priority value the sooner the callback will be fired. Events having the same priority are called in the
181-
* order they were added.
182182
* @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered
183183
* listener before being dispatched to any EventTarget beneath it in the DOM tree.
184-
*
185-
* @method module:utils/dom/emittermixin~ProxyEmitter#on
186184
*/
187-
on( event, callback, options = {} ) {
188-
// Execute parent class method first.
189-
EmitterMixin.on.call( this, event, callback, options );
190-
185+
attach( event, callback, options = {} ) {
191186
// If the DOM Listener for given event already exist it is pointless
192187
// to attach another one.
193188
if ( this._domListeners && this._domListeners[ event ] ) {
@@ -211,34 +206,27 @@ extend( ProxyEmitter.prototype, EmitterMixin, {
211206
/**
212207
* Stops executing the callback on the given event.
213208
*
209+
* @method module:utils/dom/emittermixin~ProxyEmitter#detach
214210
* @param {String} event The name of the event.
215-
* @param {Function} callback The function to stop being called.
216-
*
217-
* @method module:utils/dom/emittermixin~ProxyEmitter#off
218211
*/
219-
off( event, callback ) {
220-
// Execute parent class method first.
221-
EmitterMixin.off.call( this, event, callback );
222-
212+
detach( event ) {
223213
let events;
224214

225215
// Remove native DOM listeners which are orphans. If no callbacks
226216
// are awaiting given event, detach native DOM listener from DOM Node.
227-
// See: {@link on}.
217+
// See: {@link attach}.
228218

229219
if ( this._domListeners[ event ] && ( !( events = this._events[ event ] ) || !events.callbacks.length ) ) {
230220
this._domListeners[ event ].removeListener();
231221
}
232222
},
233223

234224
/**
235-
* Create a native DOM listener callback. When the native DOM event
225+
* Creates a native DOM listener callback. When the native DOM event
236226
* is fired it will fire corresponding event on this ProxyEmitter.
237227
* Note: A native DOM Event is passed as an argument.
238228
*
239229
* @private
240-
* @param {String} event
241-
*
242230
* @method module:utils/dom/emittermixin~ProxyEmitter#_createDomListener
243231
* @param {String} event The name of the event.
244232
* @param {Boolean} useCapture Indicates whether the listener was created for capturing event.
@@ -251,7 +239,7 @@ extend( ProxyEmitter.prototype, EmitterMixin, {
251239

252240
// Supply the DOM listener callback with a function that will help
253241
// detach it from the DOM Node, when it is no longer necessary.
254-
// See: {@link off}.
242+
// See: {@link detach}.
255243
domListener.removeListener = () => {
256244
this._domNode.removeEventListener( event, domListener, useCapture );
257245
delete this._domListeners[ event ];

src/emittermixin.js

Lines changed: 77 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,8 @@ const EmitterMixin = {
2424
/**
2525
* Registers a callback function to be executed when an event is fired.
2626
*
27-
* Events can be grouped in namespaces using `:`.
28-
* When namespaced event is fired, it additionally fires all callbacks for that namespace.
29-
*
30-
* myEmitter.on( 'myGroup', genericCallback );
31-
* myEmitter.on( 'myGroup:myEvent', specificCallback );
32-
*
33-
* // genericCallback is fired.
34-
* myEmitter.fire( 'myGroup' );
35-
* // both genericCallback and specificCallback are fired.
36-
* myEmitter.fire( 'myGroup:myEvent' );
37-
* // genericCallback is fired even though there are no callbacks for "foo".
38-
* myEmitter.fire( 'myGroup:foo' );
39-
*
40-
* An event callback can {@link module:utils/eventinfo~EventInfo#stop stop the event} and
41-
* set the {@link module:utils/eventinfo~EventInfo#return return value} of the {@link #fire} method.
27+
* Shorthand for {@link #listenTo `this.listenTo( this, event, callback, options )`} (it makes the emitter
28+
* listen on itself).
4229
*
4330
* @method #on
4431
* @param {String} event The name of the event.
@@ -49,34 +36,7 @@ const EmitterMixin = {
4936
* order they were added.
5037
*/
5138
on( event, callback, options = {} ) {
52-
createEventNamespace( this, event );
53-
const lists = getCallbacksListsForNamespace( this, event );
54-
const priority = priorities.get( options.priority );
55-
56-
callback = {
57-
callback,
58-
priority
59-
};
60-
61-
// Add the callback to all callbacks list.
62-
for ( const callbacks of lists ) {
63-
// Add the callback to the list in the right priority position.
64-
let added = false;
65-
66-
for ( let i = 0; i < callbacks.length; i++ ) {
67-
if ( callbacks[ i ].priority < priority ) {
68-
callbacks.splice( i, 0, callback );
69-
added = true;
70-
71-
break;
72-
}
73-
}
74-
75-
// Add at the end, if right place was not found.
76-
if ( !added ) {
77-
callbacks.push( callback );
78-
}
79-
}
39+
this.listenTo( this, event, callback, options );
8040
},
8141

8242
/**
@@ -101,33 +61,41 @@ const EmitterMixin = {
10161
};
10262

10363
// Make a similar on() call, simply replacing the callback.
104-
this.on( event, onceCallback, options );
64+
this.listenTo( this, event, onceCallback, options );
10565
},
10666

10767
/**
10868
* Stops executing the callback on the given event.
69+
* Shorthand for {@link #stopListening `this.stopListening( this, event, callback )`}.
10970
*
11071
* @method #off
11172
* @param {String} event The name of the event.
11273
* @param {Function} callback The function to stop being called.
11374
*/
11475
off( event, callback ) {
115-
const lists = getCallbacksListsForNamespace( this, event );
116-
117-
for ( const callbacks of lists ) {
118-
for ( let i = 0; i < callbacks.length; i++ ) {
119-
if ( callbacks[ i ].callback == callback ) {
120-
// Remove the callback from the list (fixing the next index).
121-
callbacks.splice( i, 1 );
122-
i--;
123-
}
124-
}
125-
}
76+
this.stopListening( this, event, callback );
12677
},
12778

12879
/**
12980
* Registers a callback function to be executed when an event is fired in a specific (emitter) object.
13081
*
82+
* Events can be grouped in namespaces using `:`.
83+
* When namespaced event is fired, it additionally fires all callbacks for that namespace.
84+
*
85+
* // myEmitter.on( ... ) is a shorthand for myEmitter.listenTo( myEmitter, ... ).
86+
* myEmitter.on( 'myGroup', genericCallback );
87+
* myEmitter.on( 'myGroup:myEvent', specificCallback );
88+
*
89+
* // genericCallback is fired.
90+
* myEmitter.fire( 'myGroup' );
91+
* // both genericCallback and specificCallback are fired.
92+
* myEmitter.fire( 'myGroup:myEvent' );
93+
* // genericCallback is fired even though there are no callbacks for "foo".
94+
* myEmitter.fire( 'myGroup:foo' );
95+
*
96+
* An event callback can {@link module:utils/eventinfo~EventInfo#stop stop the event} and
97+
* set the {@link module:utils/eventinfo~EventInfo#return return value} of the {@link #fire} method.
98+
*
13199
* @method #listenTo
132100
* @param {module:utils/emittermixin~Emitter} emitter The object that fires the event.
133101
* @param {String} event The name of the event.
@@ -137,7 +105,7 @@ const EmitterMixin = {
137105
* the priority value the sooner the callback will be fired. Events having the same priority are called in the
138106
* order they were added.
139107
*/
140-
listenTo( emitter, event, callback, options ) {
108+
listenTo( emitter, event, callback, options = {} ) {
141109
let emitterInfo, eventCallbacks;
142110

143111
// _listeningTo contains a list of emitters that this object is listening to.
@@ -180,7 +148,34 @@ const EmitterMixin = {
180148
eventCallbacks.push( callback );
181149

182150
// Finally register the callback to the event.
183-
emitter.on( event, callback, options );
151+
createEventNamespace( emitter, event );
152+
const lists = getCallbacksListsForNamespace( emitter, event );
153+
const priority = priorities.get( options.priority );
154+
155+
const callbackDefinition = {
156+
callback,
157+
priority
158+
};
159+
160+
// Add the callback to all callbacks list.
161+
for ( const callbacks of lists ) {
162+
// Add the callback to the list in the right priority position.
163+
let added = false;
164+
165+
for ( let i = 0; i < callbacks.length; i++ ) {
166+
if ( callbacks[ i ].priority < priority ) {
167+
callbacks.splice( i, 0, callbackDefinition );
168+
added = true;
169+
170+
break;
171+
}
172+
}
173+
174+
// Add at the end, if right place was not found.
175+
if ( !added ) {
176+
callbacks.push( callbackDefinition );
177+
}
178+
}
184179
},
185180

186181
/**
@@ -189,7 +184,7 @@ const EmitterMixin = {
189184
* * To stop listening to a specific callback.
190185
* * To stop listening to a specific event.
191186
* * To stop listening to all events fired by a specific object.
192-
* * To stop listening to all events fired by all object.
187+
* * To stop listening to all events fired by all objects.
193188
*
194189
* @method #stopListening
195190
* @param {module:utils/emittermixin~Emitter} [emitter] The object to stop listening to. If omitted, stops it for all objects.
@@ -211,13 +206,14 @@ const EmitterMixin = {
211206

212207
// All params provided. off() that single callback.
213208
if ( callback ) {
214-
emitter.off( event, callback );
209+
removeCallback( emitter, event, callback );
215210
}
216211
// Only `emitter` and `event` provided. off() all callbacks for that event.
217212
else if ( eventCallbacks ) {
218213
while ( ( callback = eventCallbacks.pop() ) ) {
219-
emitter.off( event, callback );
214+
removeCallback( emitter, event, callback );
220215
}
216+
221217
delete emitterInfo.callbacks[ event ];
222218
}
223219
// Only `emitter` provided. off() all events for that emitter.
@@ -246,7 +242,7 @@ const EmitterMixin = {
246242
* @param {String|module:utils/eventinfo~EventInfo} eventOrInfo The name of the event or `EventInfo` object if event is delegated.
247243
* @param {...*} [args] Additional arguments to be passed to the callbacks.
248244
* @returns {*} By default the method returns `undefined`. However, the return value can be changed by listeners
249-
* through modification of the {@link module:utils/eventinfo~EventInfo#return}'s value (the event info
245+
* through modification of the {@link module:utils/eventinfo~EventInfo#return `evt.return`}'s property (the event info
250246
* is the first param of every callback).
251247
*/
252248
fire( eventOrInfo, ...args ) {
@@ -277,7 +273,7 @@ const EmitterMixin = {
277273
// Remove the called mark for the next calls.
278274
delete eventInfo.off.called;
279275

280-
this.off( event, callbacks[ i ].callback );
276+
removeCallback( this, event, callbacks[ i ].callback );
281277
}
282278

283279
// Do not execute next callbacks if stop() was called.
@@ -508,7 +504,6 @@ function createEventNamespace( source, eventName ) {
508504
// Gets an array containing callbacks list for a given event and it's more specific events.
509505
// I.e. if given event is foo:bar and there is also foo:bar:abc event registered, this will
510506
// return callback list of foo:bar and foo:bar:abc (but not foo).
511-
// Returns empty array if given event has not been yet registered.
512507
function getCallbacksListsForNamespace( source, eventName ) {
513508
const eventNode = getEvents( source )[ eventName ];
514509

@@ -570,6 +565,25 @@ function fireDelegatedEvents( destinations, eventInfo, fireArgs ) {
570565
}
571566
}
572567

568+
// Removes callback from emitter for given event.
569+
//
570+
// @param {module:utils/emittermixin~Emitter} emitter
571+
// @param {String} event
572+
// @param {Function} callback
573+
function removeCallback( emitter, event, callback ) {
574+
const lists = getCallbacksListsForNamespace( emitter, event );
575+
576+
for ( const callbacks of lists ) {
577+
for ( let i = 0; i < callbacks.length; i++ ) {
578+
if ( callbacks[ i ].callback == callback ) {
579+
// Remove the callback from the list (fixing the next index).
580+
callbacks.splice( i, 1 );
581+
i--;
582+
}
583+
}
584+
}
585+
}
586+
573587
/**
574588
* Interface representing classes which mix in {@link module:utils/emittermixin~EmitterMixin}.
575589
*

0 commit comments

Comments
 (0)