Skip to content

Instantly share code, notes, and snippets.

@ThomasBurleson
Created June 9, 2011 17:26
Show Gist options
  • Save ThomasBurleson/1017230 to your computer and use it in GitHub Desktop.
Save ThomasBurleson/1017230 to your computer and use it in GitHub Desktop.
Cool Techniques with AS3 Closures
// ------------------------------------------------------------------------------------------------
/**
* Why CLOSURES are great!
*
* Think of Closures as Functions within Functions... where nested functions have access to parent function variables
* and arguments. So, you may ask?
*
* Closures allow developers to temporarily cache data!
* Closures can simplify recursion or iterations (aka visitors pattern)
* Closures can solve real-world `callback` problems; especially powerful for asynchronous callbacks.
*
* Read on to learn how closures can be critical to your software development.
* (I also encourage reading http://blog.morrisjohns.com/javascript_closures_for_dummies.html)
*
* Facts on Closures:
*
* The inner functions must be instantiated (memory used) each time the outer function is called.
* Closures may create VERY hard to find memory leaks
* Use of Anonymous closures in Flex/AS3 is cautioned due to occasional scope chaining issues.
* Deeply nested closures can lock large snapshots of stack data into memory.
*
* Closures are like methods, except to developers we can think of Closures as NESTING of method
* definitions. So the inner code can reference variables and arguments contained with outer closures!
*
*/
/**
* Example #1 - Closures with Iterations
*
* Challenge -
* Use Closure to convert string list of options to BitFlag equivalent
* The closure uses the local variable `invalidationFlags` to aggregate
* values during the forEach() iterations.
*
* Very nice. Notice the use of function chaining also.
*
* This example demonstrates that Closures are NOT just useful for Asynchronous activity
* but can also be invaluable for iterations and recursions.
*/
public function convertInvalidations(options:String=null):uint {
var invalidationFlags : uint = 0;
options ||= "displaylist,properties,size";
options
.split (",")
.map (
function ( item:String, index:int, array:Array ):uint
{
switch( StringUtil.trim( item ).toLowerCase() )
{
case "displaylist": return 1;
case "size": return 2;
case "properties": return 4;
default: throw new Error( "Unsupported option specified." );
}
})
.forEach (
function ( flag:uint, index:int, array:Array ):void
{
invalidationFlags = invalidationFlags | flag;
});
trace("BitFlag value for " + options + " === " + invalidationFlags);
return invalidationFlags;
}
// ------------------------------------------------------------------------------------------------
/**
* Example #2 - Closures for async event handlers (super simple usage)
*
* Challenge - Make asynchronous call. Update model when response is recevied.
*
* In this example, I am asynchronously loading a list of employees using a service layer EmployeeService
* The EmployeeService requires a constructor Responder argument that supports callbacks to deliver loaded data.
* Below onResult_loadEmployees( ) is called to aynchronously deliver a list of employees (once loaded).
*
* Load all Employees.
*
* @param criteria String that specifies filter criteria
* @retrun AsynToken
*/
public function loadEmployees(criteria:String=null) : AsyncToken
{
// onResult_loadEmployees used as a constructor parameter is a Function (or Closure) reference.
var service : EmployeeServices = new EmployeeServices( new Responder(onResult_loadEmployees) );
var token : AsyncToken = service.getEmployees( criteria );
return token;
}
/**
* Store the employee list in our EmployeeModel
*
* This is called from within EmployeeService when the server responds. Only the employee data is
* provided to this result handler; which effectively HIDES this layer from needing to `know`
* about ResultEvent types.
*
* This closure supports access to `this.model`; where model == instance of EmployeeModel
* NOTE: this looks like a class method (TRUE) and is also a closure (TRUE).
*/
private function onResult_loadEmployees(people:ArrayCollection):void {
this.model.knownEmployees = people;
}
// ------------------------------------------------------------------------------------------------
/**
* Challenge - Make asynchronous call. But now update the model with original caller arguments
* when response is recevied.
*
*
* When the list is available we also want to autoSelect the user with a specific ID.
* How do we this in our result handler?
*
* SOLUTION (a) Use private member variable to cache the value until the call returns
* BAD IDEA since multiple calls would overwrite the cached UID.!
*
* Do not do this! This is shown only to illustrate BAD ideas
*/
private var _cachedUserID : String;
public function loadEmployees(criteria:String=null, selectedID:String=null):AsyncToken
{
_cachedUserID = selectedID;
var service : EmployeeServices = new EmployeeServices( new Responder(onResult_loadEmployees) );
var token : AsyncToken = service.getEmployees( criteria );
return token;
}
/**
* Now we use the temporarily cached `_cachedUserID` in conjunction with
* our result handler logic
* Note: that the EmployeeService is responsible for calling onResult_loadEmployees()
* with an ArrayCollection instead of the ResultEvent
*/
private function onResult_loadEmployees(people:ArrayCollection):void {
model.knownEmployees = people;
if ( _cachedUserID && people.length ) {
model.selected = findUser( this._cachedUserID );
}
}
// ------------------------------------------------------------------------------------------------
/**
* Challenge - Make asynchronous call. Update model with original caller arguments
* when response is recevied.
*
*
* SOLUTION (b) Try to save the value in the TOKEN for later use.
* Fails because the token is NOT available/provided to the resultHandler.
* Also bad because it requires the resultHandler to `know` the token key used to store the value.
*
* Cannot add the argument to getEmployees(criteria, selectedID). Even if we could
* that would require an internal token usage and then the resultHandler arguments would also change.
* REALLY BAD!
*
* Do not do this!
*
* This is shown only to illustrate REALLY BAD ideas
* This is a standard solution for many Flex developers (include my old self)
*
*/
public function loadEmployees(criteria:String=null, selectedID:String=null):AsyncToken
{
var service : EmployeeServices = new EmployeeServices( new Responder(onResult_loadEmployees) );
var token : AsyncToken = service.getEmployees( criteria );
token.userID = selectedID;
return token;
}
/**
* Notice that here we have a callback that requires the ResultEvent instance since it appears to be the only safe
* way to get access to the cached value token.userID for this response.
*
* So here we did not save the value in a private member property. Instead we cached it as a dynamic attribute in the token
* instance. But what is the solution if the async call does not provide a TOKEN object?
*
* Summary: Do NOT use this appproach/solution !
*/
private function onResult_loadEmployees(event:ResultEvent):void {
model.knownEmployees = event.result as ArrayCollection;
if ( event.token.userID && people.length ) {
model.selected = findUser( event.token.userID );
}
}
// ------------------------------------------------------------------------------------------------
/**
* SOLUTION (c) Closures really solve the problem for us !!!
*
* Move the Async result handler internal to the scope of loadEmployee;
* so the local variable `selectedID` is in the closure scope.
*
* !! Totally AWESOME !!
*
* What makes this so elegant is the use of closure constructs effectively caches the `selectedID`
* value until the result handler needs it. The cache is done via the Closures's snapshot of the
* the call stack.
*/
public function loadEmployees(criteria:String=null, selectedID:String=null):AsyncToken
{
/**
* Define nested function/closure in order to preserve access to the local selectedID
* variable and the `this` variable.
*/
function onResult_loadEmployees(people:ArrayCollection):void {
model.knownEmployees = people;
// `selectedID` value is retrieved from the argument in the parent function/closure!
if ( selectedID && people.length ) {
model.selected = findUser(selectedID);
}
}
var service : EmployeeServices = new EmployeeServices( new Responder(onResult_loadEmployees) );
var token : AsyncToken = service.getEmployees( criteria );
return token;
}
@arielsom
Copy link

Thanks for the additionnal info. I had got that bit, but what you added does make it clearer for anyone else reading it :-)

@ThomasBurleson
Copy link
Author

Excellent. Thanks for the feedback and readership :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment