Dealing with asynchronous events, part 1
One of the difficulties in using an asynchronous event model is that it can be difficult to write readable code. This is true of both AJAX applications and Flex applications for the same reason.
I’ve been chipping away at this problem for the past few months, trying various alternatives. I thought it might be helpful to walk through the various coding idioms I tried, as a way of explaining the different approaches.
As an example, let’s take RPC service calls. Let’s say I am using an http service to get information about an album:
public function getAlbumInfo(albumId: int) : void { myService.request = { type: "album", albumId: albumId }; myService.send(); // I'd like to do something with the results of // my request, but I can't! }
The problem is that the results don’t come back right away. Code keeps executing and your results might not come for hundreds of milliseconds. Furthermore, the Flash Player does not provide a way to block until the results are ready. In order to respond to the results that eventually come back, you need to trap an event.
public function getAlbumInfo(albumId: int) : void { myService.addEventListener("result", getAlbumInfoResult); myService.request = { type: "album", albumId: albumId }; myService.send(); } public function getAlbumInfoResult(event: Event) : void { // Hundreds of milliseconds later, my results // have arrived, and I can add them to my list! myAlbumList.addAlbum(event.result.album); }
That’s not so bad, is it? Now, let’s imagine that I need to use the albumId inside the result function. As it turns out, the RPC send() methods have a special object called a call object that lets us do just that.
~
Using the call object to pass arguments
The call object is an object that will be passed to the result handler once the result event fires. It is a dynamic object and you can stash any data you want on this object for later use.
public function getAlbumInfo(albumId: int) : void { myService.addEventListener("result", getAlbumInfoResult); myService.request = { type: "album", albumId: albumId }; // Make the call, and save a value for later use. var call : Object = myService.send(); call.albumId = albumId; } public function getAlbumInfoResult(event: Event) : void { // Use the albumId value that was passed to me from above // as part of the call object. myAlbumList.addAlbum(event.call.albumId, event.result.album); }
Now, let’s imagine I have to chain these calls together. It can start getting pretty messy.
public function getAlbumInfo(albumId: int) : void { myService.addEventListener("result", getAlbumInfoResult); myService.request = { type: "album", albumId: albumId }; // Save the albumId for use later! var call : Object = myService.send(); call.albumId = albumId; } public function getAlbumInfoResult(event: Event) : void { var artistId: int = event.result.album.artistId; myAlbumList.addAlbum(event.call.albumId, event.result.album); myService.addEventListener("result", getArtistInfoResult); myService.request = { type: "artist", artistId : artistId }; // Now, save the artistId for use later! var call = myService.send(); call.artistId = artistId; } public function getArtistInfoResult(event: Event) : void { myArtistList.addArtist(event.call.artistId, event.result.artist); }
Next, let’s throw in a further complication: what if multiple callers need to invoke the same HTTP service? How do you ensure that you are handling the results correctly?
The problem with mulitple calls
Let’s make a simple change to the above code to illustrate the problem.
public function getAlbumInfo(albumId: int) : void { // Wire up my result handler. myService.addEventListener("result", getAlbumInfoResult); // Ask for the album info. myService.request = { type: "album", albumId: albumId }; myService.send(); // Also ask for the album art. myService.request = { type: "albumArt", albumId: albumId }; myService.send(); } public function getAlbumInfoResult(event: Event) : void { // At this point, I have no idea whether I should be // handling the album info or the album art. }
The simple-minded approach to this problem might be to wire up the first function before doing the first send, and wiring up the second function before doing the second send:
public function getAlbumInfo(albumId: int) : void { // Wire up my result handler. myService.addEventListener("result", getAlbumInfoResult); // Ask for the album info. myService.request = { type: "album", albumId: albumId }; myService.send(); // BUG!! This will not work! myService.removeEventListener("result", getAlbumInfoResult); myService.addEventListener("result", getAlbumArtResult); // Also ask for the album art. myService.request = { type: "albumArt", albumId: albumId }; myService.send(); }
If you’re familiar with asynchonous programming, the problem will be apparent to you immediately. For those who are not, here is the sequence of events that will occur:
- A listener for “result” is wired up to getAlbumInfoResult().
- The album info request is made.
- The earlier listener is removed.
- A new listener for “result” is wired up to getAlbumArtResult().
- The album art request is made.
- A bunch of time passes.
- Depending on the vagaries of the network, either the album info result or the album art result comes back.
- No matter which call returns, it will be routed to getAlbumArtResult(), because that is the only listener registered at this time.
Solving the multiple calls problem
The traditional way to solve the multiple calls problem is to attach the callback function to the call object. This works because a unique call object is created each time the service is invoked. When the result is returned from the server, the exact same call object is handed to the result handler, even if multiple calls were made.
public function doInit() { myService.addEventListener("result", handleResult); } public function getAlbumInfo(albumId: int) : void { var call : Object; // Ask for the album info. myService.request = { type: "album", albumId: albumId }; call = myService.send(); call.handler = getAlbumInfoResult; // Also ask for the album art. myService.request = { type: "albumArt", albumId: albumId }; call = myService.send(); call.handler = getAlbumArtResult; } public function handleResult(event: ResultEvent) : void { // Retrieve the call object from the event. var call : Object = event.call; // Get the handler. var handler : Function = call.handler; // Call the handler. We use handler.call(xxx) instead // of handler(xxx) to properly deal with the scope chain. handler.call(null, event); }
In this new version, the same handler is being called for both invocations of the service, but the actual handler that gets called is the one that has been stashed on the call object. In the case of the first call, getAlbumInfoResult() will get called, whereas in the second case, getAlbumArtResult() will be called.
Hi Sho, great article, I am just a bit confused by the order of these statements:
call = myService.send();
call.handler = getAlbumInfoResult;
Shouldn’t you assign the vars to the call object before sending? Even though its probably unlikely (impossible?) that the return would arrive before the var was added, it reminds me of the practice of defining your onLoad method before calling load() back in the LoadVars days.
I am also hazy on handler.call(null, event); Is that the call object or a method named call? Could you clarify a bit?
Thanks!
Great questions, Ben.
It would be clearer if we could reverse the order of the two lines of code above, but there is no way to do that because the first line is the one that generates the call object.
As it turns out, there is no way that the return can arrive before the var is added, because all ActionScript code executes in a single thread.
For example, let’s suppose that you make an HTTP request in response to someone clicking on a button. All the lines of code for my click handler are guaranteed to execute before my result handler is called.
public function onClick(event: Event) : void
{
myService.addEventListener(“result”, onResult);
myService.send();
callFunctionOne();
callFunctionTwo();
// …
callFunctionOneMillion();
// At this point, my click handler is done, and
// control goes back to the event loop. The next
// thing that will happen is that my result
// handler will be called.
}
As for the handler.call(xxx), this is an example of confusing naming.
*whew* This is all a bit confusing (which is why I wrote the post in the first place). I think I may need to clarify a bit more in a followup post.
Thanks for the clarification, that helps. Regarding Function::call(), I thought that was the case, I was just having trouble because I always use apply() and also wasn’t sure if maybe things had changed in AS3.
Maybe just changing the code to var callObject : Object = event.call; would help clarify a bit.
Thanks again,
Ben
[…] about | contact « Dealing with asynchronous events, part 1 16May2006 […]
[…] Note that this is a more basic (and common?) case than the stuff I was talking about earlier. For more advanced solutions, see here, here and here. From: XXXX@XXXXXXXXX […]
[…] Dealing with asynchronous events, part 1 […]
[…] 原文地å€ï¼šhttp://kuwamoto.org/2006/05/16/dealing-with-asynchronous-events-part-1/ […]
[…] of articles covering the issues of “Dealing with asynchronous events”. So far, he has Part 1, Part 2, and Part 3 available. I strongly recommend reading these articles. They provide a lot of […]