Inside ActionScript 3.0 Event Handling

May 26, 2009      Permalink     Tags:  actionscript-3

If you’ve programmed in ActionScript 3.0, you’re undoubtedly familiar with the following code:

someObject.addEventListener(Event.COMPLETE, completeHandler);

You may or may not know that addEventListener() takes five parameters, the last three of which are optional.

addEventListener(type:String, listener:Function, useCapture:Boolean=false, priority:int=0, useWeakReference:Boolean=false)

Have you ever wondered what the last three parameters do? In this article I take a closer look at events and event handling in ActionScript 3.0. Hopefully, by more fully understanding these features, you’ll be better able to leverage the language’s robust event architecture in your projects.

Event Flow

When a display object dispatches an event, the event will pass through three phases, which together comprise the event flow_: the capture phase, the target phase, and the bubbling phase. In ActionScript, these phases are represented by the constants EventPhase.CAPTURING_PHASE, EventPhase.AT_TARGET, and @EventPhase.BUBBLINGPHASE@. When handling an event, its current phase can be determined by examining its eventPhase property.

Capture Phase

First, during the capture phase, all ancestors in the event target’s display hierarchy are notified in reverse order, beginning with the top-level root ancestor and ending with the immediate parent of the event target. This gives ancestors a chance to perform some processing on the event object before the display object that dispatched the event itself is notified.

Target Phase

Next, once all ancestor listeners registered for the capture phase have been notified of the event, the event target itself will be notified. At this point, the event is said to be in the target phase. During the target phase, all listeners registered directly with the event target will be notified. By default, these listeners will be triggered in the order in which they registered with the event target. This order can be altered by passing an integer to the addEventListener() method’s priority parameter. The default event priority is 0. Among listeners registered for the same event type on the same object, those with the greatest priority will be notified first, and those will the lowest priority last, regardless of the order in which they registered with the object. To ensure that an event listener is notified only after all other listeners registered with the same object are notified, you can specify a negative priority. Do note that event priority only affects the order in which listeners are notified during the target phase; the order in which ancestor listeners are notified during the capture and bubbling phases cannot be altered.

Bubbling Phase

Finally, if the event is a bubbling event, meaning its bubbles property is true, all ancestors in the event target’s display hierarchy will be notified beginning with the immediate parent of the event target and bubbling up to the root ancestor. This is known as the bubbling phase. To register ancestor listeners on non-bubbling events, you must register to be notified during the capture phase, as there will be no bubbling phase:

// We pass true to the useCapture parameter
someObject.addEventListener(Event.COMPLETE, completeHandler, true);

target versus currentTarget

An event’s target property is a reference to the object that dispatched the event. For an event targeted at a nondisplay object, such as an instance of the URLLoader class, the event’s target and currentTarget properties will always be equal. For an event targeted at a display object, the event’s currentTarget property is a reference to the object whose listener is currently processing the event object. When an event is in the target phase, its currentTarget will be same as its target. When an event is in the capture or bubbling phases, currentTarget could be the event target or any of its display ancestors with registered listeners. You will never set the target or currentTarget properties: they are read-only properties set by Flash Player at runtime.

Manipulating Event Behavior

Some ActionScript events have a default behavior. For example, when a text field dispatches a TextEvent.TEXT_INPUT, the default behavior is for the entered text to appear in the text field. If an event is cancelable (i.e., its cancelable property is true), its default behavior can be canceled at runtime with the Event class’s preventDefault() method. The following example illustrates the effects of preventing the default behavior for a TextEvent.TEXT_INPUT event.

package {

import flash.display.Graphics;
import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.events.TextEvent;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.text.TextFieldType;
 
[SWF(width="150", height="40", frameRate="40", backgroundColor="0x333333")]
public class Events extends Sprite {
  public function Events() {
    init();
  }
 
  private var enabled:Boolean = true;
  private var textField:TextField = null;
  private var indicator:Sprite = null;
 
  private function init():void {
    textField = new TextField();
    textField.type = TextFieldType.INPUT;
    textField.border = true;
    textField.borderColor = 0x000000;
    textField.background = true;
    textField.backgroundColor = 0xFFFFFF;
    textField.width = 100;
    textField.height = 20;
    textField.x = 10;
    textField.y = 10;
 
    addChild(textField);
 
    textField.addEventListener(TextEvent.TEXT_INPUT, textInputHandler,
      false, 0, true);
 
    indicator = new Sprite();
    indicator.buttonMode = true;
    indicator.useHandCursor = true;
    indicator.x = 130;
    indicator.y = 20;
 
    addChild(indicator);
    updateIndicator();
 
    indicator.addEventListener(MouseEvent.CLICK, clickHandler,
      false, 0, true);
  }
 
  private function textInputHandler(event:TextEvent):void {
    // If enabled is false, prevent default event behavior. As a result,
    // no text is entered into the TextField.
    if (!enabled)
      event.preventDefault();
  }
 
  private function clickHandler(event:MouseEvent):void {
    enabled = !enabled;
    textField.text = "";
    updateIndicator();
  }
 
  private function updateIndicator():void {
    var g:Graphics = indicator.graphics;
 
    g.clear();
    g.lineStyle(1, 0x000000, 1);
    g.beginFill(enabled ? 0x009900 : 0x990000, 1);
    g.drawCircle(0, 0, 10);
    g.endFill();
  }
}
}

Many ActionScript events are not cancelable; in these cases, calling preventDefault() will have no effect on the event’s behavior.

In addition to being able to prevent an event’s default behavior, you can stop an event dispatch altogether. To accomplish this, the Event class provides us with two methods: stopPropagation() and stopImmediatePropagation(). When an event’s stopPropogation() method is invoked, the event dispatch is stopped once all registered listeners on the node that is currently processing the event object have returned. Invoking stopImmediatePropagation() on an event will stop the event dispatch immediately, and no remaining event listeners will be notified.

Event Listeners and Performance Considerations

By default, when an event listener is registered with an object, the object keeps a reference to the event listener in an internal listener list. Furthermore, the object will retain this reference to the event listener throughout the execution of the program, even after there are no other references to the listener. Consider the following:

var o:Object = {};
o.listener = function(event:MouseEvent):void {
  trace("clicked!");
};
 
var s:Sprite = new Sprite();
s.addEventListener(MouseEvent.CLICK, o.listener);
 
// Even though we explicitly set o to null, s retains a reference to
// listener, which prevents o from being garbage collected!
o = null;

This behavior keeps the object to which the listener belongs in memory even after it has been explicitly set to null. It shouldn’t take long to see how this could cause memory management issues!

ActionScript does give us one way to get around this problem, in the form of addEventListener() method’s fifth and final parameter, useWeakReference. When true, the listener registered with the object can become eligible for garbage collection. Altering the previous example to take advantage of useWeakReference makes o.listener, and, consequently, o eligible for garbage collection.

var o:Object = {};
o.listener = function(event:MouseEvent):void {
  trace("clicked!");
};
 
var s:Sprite = new Sprite();
 
// Here we register the listener with useWeakReference set to true
s.addEventListener(MouseEvent.CLICK, o.listener, false, 0, true);
 
// Now o is eligible for garbage collection as we'd expect
o = null;

Just because o.listener is eligible for garbage collection, however, doesn’t necessarily mean it will be garbage collected. The solution is to explicitly unregister the event listener with the object, using addEventListener()’s counterpart, removeEventListener(), which has the following signature:

removeEventListener(type:String, listener:Function, useCapture:Boolean=false);

Note the optional third parameter, useCapture. Look familiar? It’s the same as addEventListener()’s third parameter. This is by design: you must pass the same value for useCapture to removeEventListener() when unregistering a listener as was passed to addEventListener() when registering the listener. Given we have a listener,

private function clickHandler(event:MouseEvent):void {
  // Determine the event's current phase:
  if (event.eventPhase == EventPhase.CAPTURING_PHASE) {
    // Handle event in capture phase
  }
  else if (event.eventPhase == EventPhase.BUBBLING_PHASE) {
    // Handle event in bubbling phase
  }
}

consider the following:

// clickHandler() is registered for the capture phase
container.addEventListener(MouseEvent.CLICK, clickHandler, true);
 
// ...and here it is registered again for the bubbling phase
container.addEventListener(MouseEvent.CLICK, clickHandler);

clickHandler() is registered twice with container: once for the capture phase and once for the bubbling phase. This means that two references to clickHandler() are stored in container‘s internal listener list. When container dispatches a MouseEvent.<span class="caps">CLICK</span> event, clickHandler() will be notified twice. Now let’s unregister clickHandler():

// Unregister clickHandler() for the bubbling phase
container.removeEventListener(MouseEvent.CLICK, clickHandler);
 
// ...and we must unregister it again for the capture phase
container.removeEventListener(MouseEvent.CLICK, clickHandler, true);

When clickHandler() is unregistered with container the first time, useCapture is not specified, so the default value of false is assumed. If we didn’t explicitly unregister clickHandler() again for the capture phase (i.e., passing true for useCapture), it would still receive subsequent MouseEvent.<span class="caps">CLICK</span> events dispatched by container.

You should make a habit of always removing event listeners when you no longer need them. Consider the following example:

public class Example extends Sprite {
  public function Example() {
    stage.addEventListener(MouseEvent.CLICK,
      function(event:MouseEvent):void {
        // Do something with the click event
      });
  }
}

Can you see the problem? The curious reader will notice that we cannot unregister the event listener because there is no reference to it; it is stranded! In this case, the stage will retain a reference to the anonymous function as long as the program is running, long after @Example@’s constructor has returned. To avoid this problem, simply don’t use anonymous listeners. This is much better:

public class Example extends Sprite {
   public function Example() {
      // Because we have a reference to clickHandler(),
      // we can unregister it later
      stage.addEventListener(MouseEvent.CLICK, clickHandler);
   }
   
   private function clickHandler(event:MouseEvent):void {
      // Do something with the click event
   }
}

Referenced Works:

  1. Moock, Colin. Essential ActionScript 3.0. Cambridge: O’Reilley, 2007.
  2. Adobe Flex 3.3 Language and Components Reference. Feb. 2009. Adobe Systems, Inc. 17 May 2009 <http://livedocs.adobe.com/flex/3/langref/>.

Other AS3 Event Resources:

  1. McCauley, Trevor. Introduction to Event Handling in ActionScript 3.0. Feb. 2007. Adobe Developer Connection. 25 May 2009 <http://www.adobe.com/devnet/actionscript/articles/
    event_handling_as3.html
    >

I'm a Sr. UI Developer at Electric Cloud. I'm interested in full-stack JavaScript, Rails, web development, and startups.

Find me on Twitter: @bryandragon