Asynchronous operations in AS3.
##About
Asynchronous AS3 tasks are intended to simplify event flow usually in loading or animation sequences.
- Asynchronous tasks are very close to JS Promises however they are written in more object oriented style.
- There are wrappers for commonly used Flash API.
- No events are created and dispatched, we use direct callbacks because they are more fast.
- Error handling is provided.
- Synchronous functions can be used as asynchronous tasks.
Asynchronous sequence is defined using fluent interface where each method accepts ITask
or Function
. For example the following code defines a sequence that loads "some.swf", then "some.xml" and calls the callback function when all tasks are complete:
async(new LoaderTask("some.swf"))
.then(new URLLoaderTask("some.xml"))
.then(function():void {trace("complete");});
Once the sequence is defined it should be started or stored in a variable:
var s:IAsync = async(new LoaderTask("some.swf"))
.then(new URLLoaderTask("some.xml"))
.then(function():void {trace("complete");});
s.await();
Note that the sequence can run only once. It can not be started again if complete or canceled.
Each task in the sequence has input and output parameters (like single argument and return value in a function). The output of previous task is input of the next task and so on. The first argument in the sequence can be passed to await
method. The following example uses functions to display arguments, however you can define an asynchronous task that will be using arguments the same way:
async(function(arg:String):void {trace(arg);}) // start-sequence
.then(new LoaderTask("some.swf"))
.then(function(loader:Loader):void {trace(loader.content);}); // [object MovieClip]
.then(new URLLoaderTask("some.xml"))
.then(function(loader:URLLoader):void {trace(loader.data);}); // <some-xml/>
.await("start-sequence");
Actually you can pass your arguments directly to async
and then
method if the arguments are known when the sequence is defined:
async("start-sequence")
.then(function(arg:String):void {trace(arg);}) // start-sequence
.then(new LoaderTask("some.swf"))
.then("continue-sequence")
.then(function(arg:String):void {trace(arg);}) // continue-sequence
.then(new URLLoaderTask("some.xml"))
.await();
Notice that Error
and ErrorEvent
are considered as errors and they are "thrown" (see Handling errors section). For example here URLLoaderTask
is never executed:
async(new LoaderTask("some.swf"))
.then(new Error())
.then(new URLLoaderTask("some.xml"))
.await();
The task can complete successfully or not (like fulfill and reject in promises). If it is not successful it "throws" an error and no further tasks are executed until an error handler is found. The error handler is a task passed to method except
. For example single error handler for all tasks (note that URLLoaderTask
is not executed if URLLoaderTask
fails):
async(new LoaderTask("some.swf"))
.then(new URLLoaderTask("some.xml"))
.except(function(error:*):void {trace(error);}); // IOErrorEvent or SecurityErrorEvent
.then(function():void {trace("complete");}); // complete
.await();
Also you can define many error handlers for each of tasks (in that case URLLoaderTask
is executed even if URLLoaderTask
fails):
async(new LoaderTask("some.swf"))
.except(function(error:*):void {trace(error);}); // IOErrorEvent or SecurityErrorEvent
.then(new URLLoaderTask("some.xml"))
.except(function(error:*):void {trace(error);}); // IOErrorEvent or SecurityErrorEvent
.then(function():void {trace("complete");}); // complete
.await();
Errors thrown by functions are also handled the same way:
async(function():void {throw new Error();})
.except(function(error:*):void {trace(error);}); // Error
.then(function():void {trace("complete");}); // complete
.await();
Error handler can be any task, so for example you can execute URLLoaderTask
only if LoaderTask
fails:
async(new LoaderTask("some.swf"))
.except(new URLLoaderTask("some.xml"));
.then(function():void {trace("complete");});
.await();
When you need a task to be executed only if the previous tasks are successful you can use branching. The following example loads one or another xml depending on success of URLLoaderTask
:
async(new LoaderTask("some.swf"))
.then(new URLLoaderTask("success.xml"),
new URLLoaderTask("failure.xml"))
.then(function():void {trace("complete");}); // complete
.await();
Branching and error handling can be combined in any way. For example here we can handle errors of whatever URLLoaderTask
is executed:
async(new LoaderTask("some.swf"))
.then(new URLLoaderTask("success.xml"),
new URLLoaderTask("failure.xml"))
.except(function(error:*):void {trace(error);}); // IOErrorEvent or SecurityErrorEvent
.then(function():void {trace("complete");}); // complete
.await();
Execution of asynchronous sequence can be interrupted at any moment. But you need to keep a reference to the asynchronous sequence to do that. For example canceling loading in 100 ms regardless of which file exactly is being loaded at the moment:
var s:IAsync = async(new LoaderTask("some.swf"))
.then(new URLLoaderTask("some.xml"));
s.await();
setTimeout(function():void {s.cancel();}, 100);
When your sequence is already running you can still add new tasks to it. Those new tasks will be executed as usual:
var s:IAsync = async(new LoaderTask("some.swf"))
s.await();
s.then(new URLLoaderTask("some.xml"));
Be careful with this feature. First of all don't add error handlers to the running sequence because you can get error before adding the handler. Also call
await
after adding new tasks to make sure the sequence continues if it is already complete.
This feature can be usefull when implementing asynchronous queue that executes tasks one after another:
public class Queue {
private var _async:IAsync = async();
public function add(task:Object, onComplete:Function, onError:Function):void {
_async
.then(async(task).then(onComplete, onError))
.await();
}
}
LoaderTask(source:*, context:LoaderContext = null)
uses Loader object to load a flash movie from the given url,URLRequest
,ByteArray
object or class; returnsLoader
object; throwsIOErrorEvent
,SecurityErrorEvent
.URLLoaderTask(source:Object, format:String = "text")
uses URLLoader to load a file from the given url orURLRequest
; returnsURLLoader
object; throwsIOErrorEvent
,SecurityErrorEvent
.
TimeoutTask(milliseconds=0)
waits for the given number of milliseconds.FramesTask(frames=0)
waits for the given number of frames.
The following tasks use FileReference to browse, download or upload files:
BrowseFileTask(filters:Array = null)
returnsFileReference
with the selected file or throwsEvent.CANCEL
if no file is selected.DownloadFileTask(source:Object, defaultFileName:String = null)
returnsFileReference
with the downloaded file or throwsEvent.CANCEL
,IOErrorEvent
,SecurityErrorEvent
.UploadFileTask(source:Object, dataFieldName:String = "Filedata")
getsFileReference
as argument and uploads it to the given url, must be called afterBrowseFileTask
.LoadFileTask()
getsFileReference
as argument and loads file content, must be called afterBrowseFileTask
.
async
function itself returns a task so you can simply pass one async
into another. This is useful in case of complex error handling and branching. For example we can load two files sequentially if the main task fails:
async(new LoaderTask("some.swf"))
.except(async(new URLLoaderTask("file1.xml"))
.then(new URLLoaderTask("file2.xml")))
.then(function():void {trace("complete");});
.await();
Factory function allows to define a task based on result of the previous task. The function should return ITask
. For example load xml or json files based on config value:
async(new URLLoaderTask("config.txt"))
.then(function(loader:URLLoader):ITask {
if (loader.data == "load-xml") {
return async(new URLLoaderTask("file1.xml"))
.then(new URLLoaderTask("file2.xml"));
} else {
return new URLLoaderTask("file.json");
}
})
.then(function():void {trace("complete");});
.await();
Using factory function you can keep an instance of Task
in closure and return or throw depending of the result of your asynchronous method. The following example waits for 100 ms and returns or throws based on config value:
async(new URLLoaderTask("config.txt"))
.then(function(loader:URLLoader):ITask {
var task:Task = new Task();
if (loader.data == "ok") {
setTimeout(function():void {task.onReturn("Success");}, 100);
} else {
setTimeout(function():void {task.onThrow(new Error());}, 100);
}
return task;
})
.except(function(error:*):void {trace(error);}) // Error
.then(function(value:*):void {trace(value);}); // Success
.await();
If you want to implement completely custom task it could be done by extending Task
class. Then you will be able to use that task in a sequence, await and cancel it like any other task. The following example defines a task that waits for 100 ms and returns or throws based on the given arguments:
public class MyTask extends Task {
private var _timeoutId;
override protected function onAwait():void {
if (args == "ok") {
_timeoutId = setTimeout(function():void {onReturn("Success");}, 100);
} else {
_timeoutId = setTimeout(function():void {onThrow(new Error());}, 100);
}
}
override protected function onCancel():void {
clearTimeout(_timeoutId);
}
}
If you want to implement a custom task that extends your base class you can implement ITask
interface and use inner Task
object. The following example defines a task that waits for 100 ms and returns or throws based on the given arguments:
public class MyTask extends MyBaseClass implements ITask {
private var _innerTask:Task = new Task();
private var _timeoutId;
public function await(args:Object = null, result:IResult = null):void {
_innerTask.await(args, result);
if (args == "ok") {
_timeoutId = setTimeout(function():void {_innerTask.onReturn("Success", this);}, 100);
} else {
_timeoutId = setTimeout(function():void {_innerTask.onThrow(new Error(), this);}, 100);
}
}
public function cancel():void {
_innerTask.cancel();
clearTimeout(_timeoutId);
}
}
Sometimes we need to execute asynchronous tasks together and continue when all or any of them are complete.
asyncAll
function allows to define asynchronous concurrence that ends when all of the given tasks are complete. It returns an array of values in order of completion of child tasks.
async(asyncAll(async(new TimeoutTask(200)
.then("second")))
.and(async(new TimeoutTask(100)
.then("first"))));
.then(function(args:*):void {trace(args);}) // first, second
.await();
asyncAny
function allows to define asynchronous concurrence that ends when any of the given tasks is complete. It returns the value of the first complete task. Other child tasks are canceled.
async(asyncAny(async(new TimeoutTask(200)
.then("second")))
.or(async(new TimeoutTask(100)
.then("first"))));
.then(function(args:*):void {trace(args);}) // first
.await();