Skip to content

Instantly share code, notes, and snippets.

@ThomasBurleson
Created June 9, 2011 17:26
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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

awesome! Good explanations on closures are hard to find, so thanks :-) It's the bit about the caching of selectedId that I was having trouble groking.

@ThomasBurleson
Copy link
Author

The issue is that the server response does NOT provide the selectedID... it was available before the async call and is also used after the async response is received.

So how do we temporarily cache it?

If we use a instance property to cache it, what happens if that instance is used to perform another async call BEFORE the current call responds. In that case the first selectedID value has been corrupted/replaced with the value from the second selectedID

So how do we isolate and cache client-side values?

We can use the TOKEN as demonstrated in Solution (b). But that only works for async calls that generate token objects. And it requires the async handlers to use the ResultEvent as the argument instead of the actual data we want to directly use.

So what is a generic, good async solution? Closures... as demonstrated in the final example.

@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