Studio Developer Manual / Version 2301
Table Of Contents
Always remove any listeners that you attach to an Observable
,
Bean
, ValueExpression
, or any other object that emits events.
Even when using the option {single: true}
, the event might not have
been fired at all when your component is destroyed.
A typical error pattern is to attach some method handleFoo
as event
listener, but by mistake hand in another method with a similar name handleFuu
when intending to remove the listener. No error whatsoever is reported, because trying
to remove a function as listener that is not in the current set of listeners is silently
ignored by Observable#removeListener()
and all other event emitters.
A useful utility to automate removing listeners is to use Observable#mon()
instead of Observable#on()
(alias: Observable#addListener()
).
mon
does not attach the listener to the caller, but to the first parameter,
but binds it to the lifetime of the caller. For example, when your custom component
creates a DOM element elem
and registers a click
listener like so:
this.mon(elem, "click", handleClick)
, the listener is automatically
detached when your component (the caller, this
) is destroyed.
Caution
It never makes sense to call comp.mon(comp, ...)
, because when a component
is destroyed, it removes its own listeners, anyway.
Using comp.mon(comp, "destroy", handleDestroy)
even leads to the handler
never being called, because a component removes
all mon
listeners already in its beforedestroy
phase.
In contrast, comp.on("destroy", handleDestroy)
works as expected.
Not only components, but any objects that register event handlers, most prominently actions, have to detach all event handlers again.
As actions do not have a destroy
event and onDestroy
method
like components, you have to override addComponent()
and
removeComponent()
to detect when an action starts and ends being used
by any component. Introducing a simple counter field starting with zero, you
should acquire resources (for example, register event listeners, populate fields) when
addComponent()
is called while the counter is zero before increasing,
and release resources (remove event listeners, set fields to null
) when
removeComponent()
is called while the counter is zero after decreasing.
To minimize the impact in case event listeners are not detached, and to avoid cyclic dependencies, keep the scope of any event handler function or method as small as possible. In the optimal case, the event handler function is a private static method, for example if it just toggles a style class of the DOM element given in the event object:
#attachListeners():void { const el = this.getEl(); // bad style: using an anonymous function that // does not need its outer scope at all: el.addListener("mouseover", e => e.getTarget().addClass("my-hover")); // good style: for such cases, use a static method: el.addListener("mouseout", this.#removeHoverCls); } static #removeHoverCls(e:IEventObject):void { e.getTarget().removeClass("my-hover"); });
If your event handler only needs access to this
(this current component instance), declare it as
a method as opposed to an anonymous function:
private #hoverCounter:int = 0; #attachListeners():void { const el = getEl(); // bad style: using an anonymous function that // only needs to access "this": el.addListener("mouseover", e => ++this.++hoverCounter); // good style: for such cases, use a (non-static) method: el.addListener("mouseout", bind(this, this.#countHoverEvent)); } #countHoverEvent(e:IEventObject):void { ++this.hoverCounter; });
In TypeScript, like in JavaScript, anonymous or inline functions have lexical scope,
that is they can access any variable declared in the surrounding function or method.
Since this scope usually contains a reference to the object that emits events (here:
el
), and that object stores your event handler function in its listener
set, you create a cyclic reference between the two.
Cyclic references are not bad per se, because garbage collection can handle them if
all objects contained in the cycle are not referenced from "outside". But firstly,
as long as any of the objects is kept alive, all others are retained, too, and secondly,
as discussed below, this makes finding the real culprit for memory leaks harder.