September 6, 2009

AS3 Events - 7 things I've learned from the community

Since posting my initial critique of AS3 events, I've learned a fair bit through community feedback. Thanks everyone for your comments and corrections; I edited my original post accordingly. Conversations on Twitter brought out further subtleties of how developers work with AS3 events.

I still have more critique in me, but first I'll eat a little humble pie. I haven't actually worked much with custom events in the display list. Most of my AS3 development has been in code libraries that operate independently of the display list. Thus, my custom events generally don't need to bubble or cancel. So when I look at the AS3 event system, I see APIs that often add clutter without a benefit to my project. For developers building RIAs, AS3 event capturing, targeting, bubbling and canceling is wonderful. The standard is called DOM Level 3 Events for a reason. It's great if you're in a DOM, but that doesn't mean it's the most usable solution for events in general. But I'll have to leave that discussion for the next critique.

What I've learned about AS3 events in the last 3 weeks

  1. Custom events can bubble when you dispatch them from a DisplayObject.
    I'd never tried this, and thought that only Flash Player events (MouseEvent, etc.) could bubble.

  2. Custom events can be canceled.
    The Event APIs cancelable, preventDefault() and isDefaultPrevented() are not just for Flash Player events.

  3. Despite Grant Skinner's argument for using weak listeners, some experienced developers choose not to.
    Some say relying on weak references has caused more problems than anticipated. However, Grant advocated always removing listeners explicitly. Weak references are just "an added level of security".

  4. Flash Player 9 didn't always garbage collect weak references.
    This is fixed in Player 10.

  5. Storing method references in a weak keys Dictionaryis buggy.
    References may be duplicated or garbage collected prematurely. Technically, this isn't part of AS3 events. But when trying to extend the event system, you may use a Dictionary to store listeners or callbacks. Developers who've done this have learned not to use weak keys.

  6. When an event has no listeners, EventDispatcher.dispatchEvent() is unnecessarily slow.
    Grant Skinner's patch is a 5x speedup, apparently. 

  7. Some say listener priorities are smelly.
    Like MovieClip.depth in AS2, priority numbers introduce dependencies into your code. These become increasingly difficult to manage in larger systems, as new code must take into account the  priorities in existing code.

    An alternative is to rely on the order the listeners are added. The original dispatcher can add itself as a listener first and thus ensure it has first crack at the event. Unless a different listener misbehaves and steals the spotlight with a higher priority...

    This is an interesting one. I haven't had to deal with this issue and I'm not sure what I think yet. The listener order does start to feel similar to the stacking of movie clips in AS2. Who's going to come out on top? Do we need an equivalent of getNextHighestDepth() for listener priorities [shudder]? I'm reminded of how the Macromedia V2 components would grab the highest possible depth with its own depth manager, rendering getNextHighestDepth() useless. Is there a word for bad nostalgia? How about "nastalgia"?


phillip said...

Worth noting that when attempting to bubble custom events, you want to make sure your event class has an override for clone() where you build a copy the custom properties in your event and return that.

Robert Penner said...

Good point, Phillip. How often do you think EventDispatcher calls clone() when bubbling? Does it create a new instance at each level?

Keith Peters said...

As mentioned in private, I don't usually worry about making event listeners weak references. Partially due to laziness and not wanting to type out all those excess arguments when only the last is necessary, and partially due to the fact that I pretty much always take care of my listeners and remove them as needed. Code for a while in Objective-C without garbage collection and this kind of thing becomes second nature. I also don't think I've ever used priority.

xleon said...

Some time ago I spent like 3 hours to figure out why my app event flow wasn´t working. Some event was lost in the way. My app had many events going from one place to another and it was really hard to find where this issue was comming from.
After an insane time looking for everything on my code, I realised the issue was an event listener weak reference. For any reason I don´t know yet, the event was diying before getting its goal.

From that day I never use weak references on similar scenarios. Too hard to find...

xleon said...

And I had a similar problem using priority, I think the same about that, too hard to find if you have any issue.

Ben Clinkinbeard said...

clone() is only called if an event is manually re-dispatched by passing it to dispatchEvent() again. So if a parent catches it and then does dispatchEvent( childEvent ), clone() will be used and required. Bubbling alone does not call clone(), but overriding it is good practice.

Robert Penner said...

Thanks Ben, that makes perfect sense.

Jonathan Kaye said...

I noticed something interesting regarding cloning and dispatching a custom event that I can't explain. I have a class in which I create a custom event instance as a class member to dispatch. While handling a timer event defined for the class, I dispatch that event I created (so the event handler is for a different event). When I didn't create clone properly, I noticed it was dispatching an Event but not a custom event. Of course I fixed clone for my custom event, but I thought it was strange that it was cloning an event when the handler was handling a different event, so I didn't think it was "re-dispatching" the event.

gropapa said...

yes that is true, when you "re" dispatch an Event manually, the clone method() is called, and since this method does return a new Event(...) you must override it.
What i v been thinking lately is why we usually do such thing:
override public function clone():Event{
return new CustomEvent(...);

instead of writing this
override public function clone():Event{
return this;

we usually write custom events not for disoplay list but for custom classes like an MVC for example.
I have never been using weak references but it would be great that by default when the handler object is null...that the reference is deleted too! I mean we quickly have a big amount of code just putting listeners on each type we have to listene...and we hav to do the same to remove the listeners!
(Sorry for my bad english though)

phillip said...

One thing I thought I'd confirm. Using weak references on your addEventListeners has no issues if you are sure to removeEventListener() right? Naturally, I end up typing too much--but I figure I intend to do all the necessary clean up (and remove listeners) but if I forget one I'm much better off using weak references.

Maybe the advice against weak references is "don't count on them".


Robert Penner said...

I agree. You should be cleaning up your listeners. One of the goals of my Signals system reduces the amount of code necessary for clean-up. For example, many events are basically callbacks where you will remove the listener when it fires. My Signal has an API for adding one-time listeners that will be removed automatically on dispatch, so you don't have to.

sage said...

Use pureMVC, you won't have problems with Event model in Flash

Frankie said...

@sage - I think you're still going to want to dispatch regular events from your view components to their mediators so you can't escape that easily!

Danko Kozar said...

Regarding: "Some say listener priorities are smelly."

I find event priorities very useful when having components listening for their own events.

I have en example, the actual problem that I had:

I had a component overriding the button to which I added a listener to a MOUSE_DOWN event ("outside" of the component), because I wanted to do a dragging stuff in a handler.

So far, so good: the dragging worked. Now I decided to implement the RESIZING feature INTERNALLY in the override of the button. That resizing should also listen to MOUSE_DOWN event and start the resizing process if clicked, let's say, 10px from the border. Also, it should cancel the MOUSE_DOWN event, so the other listeners (like the one from the outside) should not be called.

However, after starting the app, I realized that the drag operation (the outer handler) is always called - even if I click the button border.

That is because you aren't in control of the order of handler execution like as you could have be when adding the both listeners from the "outside". When adding the listeners by yourself, they will be executed by the order you are adding them.

So, that's where priorities jump in. The internal event listener should have a higher priority as it should react before any other handler and cancel the event when clicked on border.

Of course, priorities are some integers, so it might have look dirty, or AS2-like, but you might use string constants containing most used priorities instead.

I haven't seen Flash player source (nobody has :)), but there's a dictionary-of-dictionaries inside of an event dispatcher. The outer dict has *string* keys (eventType) and each value contains another dict with *int* keys (priorities) with functions (event handlers) as values. At least I would do it like that. ^_^

Danko Kozar

Anonymous said...

Following up on Danko's comment:

The order of triggering the event handlers are:
- by event type
- by event priority
- by order added

The event's preventDefault API used in conjunction with cancelable AND event priorities are powerful ways to build components. Many of Flex's Spark component sets utilize this paradigm.