If you haven’t experimented much with closures yet – whether in your Flash/Flex projects, Javascripting or while tinkering with Lua – it’s time to start. In case you’re a little nervous about those pesky memory leaks in Flash, here are some ways to cope.
»
Much of the following code is bundled into an example Flex project that compares the various closure techniques around a custom Timer class.
Check the code on Github.
What are closures?
Many people think of closures as anonymous functions – probably because that’s the common form they take – but they are more than that. They are scoped, inline functions that provide a “closure” over a collection of free variables (within the function scope).
Check out the Wikipedia entry on closures..
Why use them?
- They allow the hiding of state (negating the need for maintaining async state in the class) as each closure defines its own variable scope that are available to all nested closures; and
- Because they’re easier to follow than continually jumping to functions defined at a type-level.
Take a peek
In the below example, we have two closures – the first defines the userEvent property and someVariable, the second adds to that with it’s own scope of serviceEvent. You see how the inner closure has access not only to that state within itself, but also to that of the outer closure, as well as that of the init() function AND the class itself. Welcome to the scope chain. Read the AS3 docs on Function Scope.
public class SomeClass
{
protected var view:ISomeView;
public function init():void
{
var functionSaysSo:Boolean = true;
userAction.addEventListener(UserEvent.LOGIN,
function(userEvent:UserEvent):void
{
//this is outer closure
//define a variable in the outer closure's scope
var someVariable:String = "something";
service.addEventListener(ServiceEvent.RESULT,
function(serviceEvent:ServiceEvent):void
{
//this is the inner closure
if (functionSaysSo)
{
view.notify(userEvent.username, serviceEvent.result, someVariable);
}
});
//async call
service.start(userEvent.username, userEvent.pass);
});
}
}
Tip: If you find yourself contesting the readability assertion from before, don’t fret – it’s early days.
Cleaning up after yourself
Like any listener, using a closure as an event listener can create memory leaks if not properly cleaned up. Luckily, we have a few options up our sleeves to avoid this.
Use weak references? [Short answer: no]
Clean, simple and easy, we could simply add closures as weak references.
userAction.addEventListener(UserEvent.LOGIN,
function():void
{
}, false, 0, true);
This keeps the code trim, however it introduces its own problems. If the variable you are listening to lives (is scoped) within another closure or a function definition, it will get cleaned up after the function completes (and before the event might fire). Without a strong reference to that variable, it is a target for garbage collection and you will end up with unpredictable results.
For example, in the following, there is nothing holding onto the timer instance to ensure after the function ends (and before the timer completes) that the timer will still exist and dispatch the TIMER_COMPLETE event.
public class SomethingWeak implements IDoesSomething
{
public function doSomething():void
{
var timer:Timer = new Timer(1000,-1);
//WARNING! Nothing is holding a reference to timer - GC candidate
timer.addEventListener(TimerEvent.TIMER_COMPLETE,
function(evt:TimerEvent):void
{
//something happened!
}, false, 0, true);
timer.start();
}
}
1. Name your handlers
To improve on this, simply define your handlers locally, and you can remove them within your listeners:
» Aug 30: Updated to inline definition
public class SomethingNamed implements IDoesSomething
{
public function doSomething():void
{
var timer:Timer = new Timer(1000,-1);
var timerHandler:Function;
timer.addEventListener(TimerEvent.TIMER_COMPLETE, timerHandler = function(evt:TimerEvent):void
{
//something happened!
//cleanup after ourselves
timer.removeEventListener(TimerEvent.TIMER_COMPLETE, timerHandler);
});
timer.start();
}
}
2. Use arguments.callee to remove them during execution
Even better, we can take advantage of a little known feature in AS3 called arguments.callee, and not even have to name our function:
public class SomethingCallee implements IDoesSomething
{
public function doSomething():void
{
var timer:Timer = new Timer(1000,-1);
timer.addEventListener(TimerEvent.TIMER_COMPLETE,
function(evt:TimerEvent):void
{
//something happened
//cleanup after ourselves
timer.removeEventListener(TimerEvent.TIMER_COMPLETE, arguments.callee);
});
timer.start();
}
}
3. Use type-level handlers to remove from separate call
Alas, what if you need to clean up based on another method or event later in the piece (say when a mediator is disposed)? You’ll need to define your handler at a type-level to retain a reference of it:
» Aug 30: Updated to inline definition
public class SomethingDisposable implements IDoesSomething, IDisposable
{
//handler is now defined at a type (class) level
private var timerHandler:Function;
//we have to also scope the timer to the type level in order to remove listeners
private var timer:Timer;
public function doSomething():void
{
timer = new Timer(1000,-1);
timer.addEventListener(TimerEvent.TIMER_COMPLETE, timerHandler = function(evt:TimerEvent):void
{
//something happened!
});
timer.start();
}
public function dispose():void
{
if (!timer) return;
timer.removeEventListener(TimerEvent.TIMER_COMPLETE, timerHandler);
//for completeness sake
timer.stop();
timer = null;
}
}
At this point, you may be wondering why bother with a closure, when you could simply define the handler as a private method? In this particular example, there is no difference unless you wanted the handler to access the timer instance itself in the handler.
4. Use Signals
There is a final alternative – use the as3-signals library. AS3 Signals is a library that provides an alternative to using Flash Events within your APIs. Using Signals, there are a handful of alternatives to clean up after your closures. Every signal implements ISignal, and it’s that interface we’ll focus on.
ISignal.addOnce()
ISignal.addOnce() prescribes attaching a handler which is called once when the signal dispatches and is removed immediately. Below we use a NativeSignal to wrap the TimerEvent.TIMER_COMPLETE, allowing us to avoid attaching and removing event listeners ourselves. We also now return a Signal which gives the user of the class a strongly-typed signal to what they expect.
public class SomethingSignalsAddOnce implements IDoesSomethingWithSignals
{
public function doSomething(index:int):ISignal
{
//create a Signal to return
const response:ISignal = new Signal(int);
const timer:Timer = new Timer(index * 100,-1);
//create a signal from the Timer event
const signal:NativeSignal = new NativeSignal(timer, TimerEvent.TIMER_COMPLETE, TimerEvent);
//once TIMER COMPLETE has occurred, we can dispatch our signal - ISignal.addOnce() ensures that any listeners to Timer will be cleaned up
signal.addOnce(function(evt:TimerEvent):void
{
//tell response that something happened (as opposed to dispatching an event, we dispatch the signal)
response.dispatch(index);
});
timer.start();
return response;
}
}
This is often very useful, but not always optimal. We may not always want to listen only once – say if we need to selectively remove the listener based on certain conditions. Sometimes we may only want to remove the listener based on another asynchronous event (as in #4 above).
Signals and arguments.callee
Alternatively, we could use the arguments.callee property and do a conditional remove when required (after 5 ticks in the below example):
public class SomethingSignalsCallee implements IDoesSomethingWithSignals
{
public function doSomething(index:int):ISignal
{
//create a Signal to return
const response:ISignal = new Signal(int);
const timer:Timer = new Timer(100);
//create a signal from the Timer event
const signal:NativeSignal = new NativeSignal(timer, TimerEvent.TIMER, TimerEvent);
var numTicks:int = 0;
signal.add(function(evt:TimerEvent):void
{
if (numTicks++ == 5)
{
response.dispatch(index);
signal.remove(arguments.callee);
timer.stop();
}
});
timer.start();
return response;
}
}
You might wonder if you can use arguments.callee within nested closures – and the answer is yes. Just be aware that each closure has its own definition of the arguments.callee, and it overrides the value from any outer closures.
ISignal.removeAll()
ISignal also expose the convenience method: removeAll(). This can help us when we need to remove listeners in response to another method call.
public class SomethingSignalsRemoveAll implements IDoesSomethingWithSignals, IDisposable
{
private var timerSignal:ISignal;
private var timer:Timer;
public function doSomething(index:int):ISignal
{
//create a Signal to return
const response:ISignal = new Signal(int);
timer = new Timer(500);
//create a signal from the Timer event
timerSignal = new NativeSignal(timer, TimerEvent.TIMER, TimerEvent);
timerSignal.add(function():void
{
response.dispatch(index);
});
timer.start();
return response;
}
public function dispose():void
{
timer.stop();
timerSignal.removeAll();
}
}
Be careful using removeAll() – if your class aggregates the signal as above, and it never leaves the containing type, fine. However, there may be occasions when you pass a signal around between various classes (as we do with the response signal above). In these classes, using removeAll() could present unwanted results if one developer inadvertently removes listeners that another class attached.
Conclusions
Whichever way you use closures, you need to remember to clean up after yourself, less you end up leaking memory in the Flash player. Asynchronous programming is here to stay (take node.js and Reactive eXtensions for .NET as examples) and we’re lucky that Actionscript – built on ECMA – supports it natively. As long as you’re aware of the consequences of attaching inline handlers, you can use closures and the async model in general to design a different approach to solving common asynchronous problems. While it takes a little getting used to, I wholeheartedly recommend giving it a shot – you might just like it.
40.714353
-74.005973
Recent Comments