代码改变世界

走在网页游戏开发的路上(五)

2011-06-06 22:17  吴秦  阅读(9088)  评论(3编辑  收藏

AS3事件模型

——AS3的灵魂之一

0.  前言

ActionScript 3.0事件模型使用方便,而且符合标准,它与Adobe Flash Player显示列表(display list)完美集成在一起。ActionScript 3.0的事件模型是基于DOM 3的事件规范[1],是业界标准的事件处理体系结构,为ActionScript 3.0程序员提供了强大而直观的事件处理工具。

为了清晰理解AS3事件模型,我们必须首先知道什么是事件模型?事件模型组成?DOM3事件模型?

事件模型是一种程式设计模型,即事件驱动程式设计Event-driven programming)。这种模型的程式执行流程是由使用者的动作(如鼠标的按键,键盘的按键动作)或者是由其他程式的讯息来决定的。事件驱动程式模型下的系统,基本上的架构是预先设计一个事件循环所形成的程序,这个事件循环程序不断地检查目前要处理的资讯,根据要处理的资讯执行一个触发函式进行必要的处理。其中这个外部资讯可能来自一个目录夹中的档案,可能来自键盘或鼠标的动作,或者是一个时间事件。事件驱动模型由以下几部分组成:

F  事件源(生产者):产生事件;

F  侦听器(消费者):监听事件的发生并作出响应;

F  事件:定义事件的特征,用于生产者、消费者之间交互;

F  事件处理者程序:用于处理事件的对象。

clip_image002

1 事件模型组成

实现事件处理器 à 监听者注册事件处理器 à 事件源发送事件 à (通过事件代理)触发监听者,接收事件 à 调用事件处理器。

2.  DOM3事件模型及事件流

DOM= Document Object Model,文档对象模型,DOM可以以一种独立于平台和语言的方式访问和修改一个文档的内容和结构。换句话说,这是表示和处理一个HTMLXML文档的常用方法。有一点很重要,DOM的设计是以对象管理组织(OMG)的规约为基础的,因此可以用于任何编程语言。最初人们把它认为是一种让JavaScript在浏览器间可移植的方法,不过DOM的应用已经远远超出这个范围。DOM技术使得用户页面可以动态地变化,如可以动态地显示或隐藏一个元素,改变它们的属性,增加一个元素等,DOM技术使得页面的交互性大大地增强。DOM实际上是以面向对象方式描述的文档模型。DOM定义了表示和修改文档所需的对象、这些对象的行为和属性以及这些对象之间的关系。可以把DOM认为是页面上数据和结构的一个树形表示,不过页面当然可能并不是以这种树的方式具体实现。

根据W3C DOM规范,DOMHTMLXML的应用编程接口(API),DOM将整个页面映射为一个由层次节点组成的文件。有1级、2级、3级共3个级别。

1DOM199810月份成为W3C的提议,由DOM核心与DOM HTML两个模块组成。DOM核心能映射以XML为基础的文档结构,允许获取和操作文档的任意部分。DOM HTML通过添加HTML专用的对象与函数对DOM核心进行了扩展。

2DOM引进了几个新DOM模块来处理新的接口类型:DOM视图——描述跟踪一个文档的各种视图(使用CSS样式设计文档前后)的接口;DOM事件——描述事件接口;DOM样式——描述处理基于CSS样式的接口;DOM遍历与范围——描述遍历和操作文档树的接口。

3DOM通过引入统一方式载入和保存文档和文档验证方法对DOM进行进一步扩展,DOM3包含一个名为“DOM载入与保存”的新模块,DOM核心扩展后可支持XML1.0的所有内容,包扩XML Infoset XPath、和XML Base

DOM事件模型设计的两个目标是:

F  允许事件监听者(event listeners)向系统注册,并可以通过一个树形结构描述事件流(event flow)。此外,规范还提供标准事件模块的用户控制接口、文档变更通知,包括每个事件模块的上下文信息定义。

F  提供当前浏览器支持事件模型的一个共同的子集,这是为了能够与现有脚本和内容互操作、共存。

DOM3事件分发机制:应用程序使用EventTarget.dispatchEvent()方法分发事件对象。但是这个方法的行为依赖于对象的事件流event flow)。事件流描述事件对象如何通过一个数据结构来传播(传送),例如当一个事件对象分发给XML文档中的一个元素时,对象传播穿越了部分文档。如下图所示,是在一个DOM树中使用DOM事件流分发事件的过程

clip_image003

2 DOM树中使用DOM事件流分发事件的过程(摘自:DOM3 event规范)

事件对象总是分发给最邻近的事件目标(proximal event targetThe proximal event target is the object representing the event target to which an event is targeted using the DOM event flow. The proximal event target is the value of the Event.target attribute.),在开始阶段必须先决定事件对象的传播路径。传播路径是事件对象必须通过的有序事件目标列表。对于树型DOM实现,传播路径必须反映文档的形结构层次。最后一个元素必须是最邻近的事件目标,列表前面的元素是目标的祖先,并且直接祖先(即前一个)是目标的父亲。

事件分为三个阶段:

F  捕获阶段(Capture Phase)——事件对象从默认视图到目标对象父亲的传播阶段,这个阶段注册的监听者在事件到达目标之前处理事件;

F  目标阶段(Target Phase)——事件对象到达目标对象的时候,这个阶段注册的事件监听者在事件到达目标时立刻处理事件;

F  冒泡阶段(Bubbling Phase)——捕获阶段的逆过程,从目标对象到默认视图的过程,这个阶段注册的事件监听者在事件到达目标之后处理事件

规范实现必须按照以下步骤让事件对象完成一个事件阶段:

首先,必须确定当前目标(current target。它应该是部分传播路径中下一个事件目标,从事件监听者的角度看,它应该是已经注册的事件目标。

然后,必须确定当前目标的候选事件监听者(candidate event listeners)。它们应该是当前目标上所有按序注册的事件监听者,一旦确定候选事件监听者将不能改变、添加、删除。

最后,按序处理所有候选事件监听者,并触发满足以下条件的监听者:

F  事件对象的传播没有即时停止

F  监听者已经在当前事件阶段注册

F  监听者已经注册了相应的事件类型

3.  AS3的事件模型

只要发生事件,Flash Player 就会调度事件对象。如果事件目标不在显示列表中,则 Flash Player 将事件对象直接调度到事件目标。例如,Flash Player progress 事件对象直接调度到 URLStream 对象。但是,如果事件目标在显示列表中,则 Flash Player 将事件对象调度到显示列表,事件对象将在显示列表中穿行,直到到达事件目标。

"事件流"说明事件对象如何在显示列表中穿行。显示列表以一种可以描述为树的层次结构形式进行组织。位于显示列表层次结构顶部的是舞台,它是一种特殊的显示对象容器,用作显示列表的根。舞台由flash.display.Stage类表示,且只能通过显示对象访问。每个显示对象都有一个名为stage的属性,该属性表示应用程序的舞台。

Flash Player调度事件对象时,该事件对象进行一次从舞台到"目标节点"的往返行程。DOM事件规范将目标节点定义为代表事件目标的节点。也就是说,目标节点是发生了事件的显示列表对象。例如,如果用户单击名为child1的显示列表对象,Flash Player将使用child1作为目标节点来调度事件对象。

从概念上来说,事件流分为三部分。第一部分称为捕获阶段,该阶段包括从舞台到目标节点的父节点范围内的所有节点。第二部分称为目标阶段,该阶段仅包括目标节点。第三部分称为冒泡阶段。冒泡阶段包括从目标节点的父节点返回到舞台的行程中遇到的节点。

如果您将显示列表想像为一个垂直的层次结构,其中舞台位于顶层(如下图显示),那么这些阶段的名称就更容易理解了:

clip_image004

如果用户单击Child1Flash Player将向事件流调度一个事件对象。如下面的图像所示,对象的行程从Stage开始,向下移动到Parent,然后移动到 Child1,再"冒泡"返回到Stage:在行程中重新经过Parent,再返回到Stage

clip_image005

在该示例中,捕获阶段在首次向下行程中包括 Stage Parent。目标阶段包括在 Child1 花费的时间。冒泡阶段包括在向上返回到根节点的行程中遇到的 Parent Stage

事件流使现在的事件处理系统比 ActionScript 程序员以前使用的事件处理系统功能更为强大。早期版本的 ActionScript 中没有事件流,这意味着事件侦听器只能添加到生成事件的对象。在 ActionScript 3.0 中,您不但可以将事件侦听器添加到目标节点,还可以将它们添加到事件流中的任何节点。

当用户界面组件包含多个对象时,沿事件流添加事件侦听器的功能十分有用。例如,按钮对象通常包含一个用作按钮标签的文本对象。如果无法将侦听器添加 到事件流,您将必须将侦听器添加到按钮对象和文本对象,以确保您收到有关在按钮上任何位置发生的单击事件的通知。而事件流的存在则使您可以将一个事件侦听器放在按钮对象上,以处理文本对象上发生的单击事件或按钮对象上未被文本对象遮住的区域上发生的单击事件。

不过,并非每个事件对象都参与事件流的所有三个阶段。某些类型的事件(例如 enterFrame init 类型的事件)会直接调度到目标节点,并不参与捕获阶段和冒泡阶段。其它事件可能以不在显示列表中的对象为目标,例如调度到 Socket 类的实例的事件。这些事件对象也将直接流至目标对象,而不参与捕获和冒泡阶段。

4.  事件实例

屏幕上嵌套三个矩形:外部outter、中部middle、内部inner,都注册监听鼠标单击事件addEventListener(MouseEvent.CLICK, clickHandler);在单击事件处理程序clickHandler中输出事件发生目标、正在侦听事件的当前目标、事件当前阶段。(例子来源于ActionScript3殿堂之路,我觉得比较经典和清晰说明了问题,故引用之。)

源码包含两个文件Main.as

   1: package 
   2: {
   3:     import flash.display.Sprite;
   4:     import flash.events.Event;
   5:     import flash.events.MouseEvent;
   6:     import flash.geom.Rectangle;
   7:     import RectContainer;    
   8:     
   9:     /**
  10:      * ...
  11:      * @author Tyler
  12:      */
  13:     public class Main extends Sprite 
  14:     {
  15:         
  16:         public function Main():void 
  17:         {
  18:             if (stage) init();
  19:             else addEventListener(Event.ADDED_TO_STAGE, init);
  20:         }
  21:         
  22:         private function init(e:Event = null):void 
  23:         {
  24:             removeEventListener(Event.ADDED_TO_STAGE, init);
  25:             // entry point
  26:             var outter:Sprite = new RectContainer(10, 10, 200, 200);
  27:             var middle:Sprite = new RectContainer(30, 30, 150, 150);
  28:             var inner:Sprite = new RectContainer(50, 50, 100, 100);
  29:             
  30:             outter.name = "外部容器";
  31:             middle.name = "中间容器";
  32:             inner.name = "内层容器";
  33:             
  34:             addChild(outter);
  35:             outter.addChild(middle);
  36:             middle.addChild(inner);
  37:             
  38:             inner.addEventListener(MouseEvent.CLICK, clickHandler);
  39:             middle.addEventListener(MouseEvent.CLICK, clickHandler);
  40:             outter.addEventListener(MouseEvent.CLICK, clickHandler);
  41:         }
  42:         
  43:         private function clickHandler(evt:MouseEvent):void
  44:         {
  45:             trace("事件发生目标:\t" + evt.target.name);
  46:             trace("正在侦听事件的当前目标\t" + evt.currentTarget.name);
  47:             trace("事件当前阶段\t"  + evt.eventPhase);
  48:             trace("==========================================");
  49:         }
  50:         
  51:     }
  52:     
  53: }

RectContainer.as:

   1: package  
   2: {
   3:     import flash.display.Sprite;
   4:     
   5:     /**
   6:      * ...
   7:      * @author Tyler
   8:      */
   9:     public class RectContainer extends Sprite 
  10:     {
  11:         
  12:         public function RectContainer(x:Number,
  13:                                         y:Number,
  14:                                         w:Number,
  15:                                         h:Number) 
  16:         {
  17:             this.graphics.beginFill(0xFFFFFF * Math.random());
  18:             this.graphics.drawRect(x, y, w, h);
  19:             this.graphics.endFill();
  20:         }
  21:         
  22:     }
  23:  
  24: }

F  点击内部矩形触发事件如下:

事件发生目标:     内层容器

正在侦听事件的当前目标 内层容器

事件当前阶段  2

==========================================

事件发生目标:     内层容器

正在侦听事件的当前目标 中间容器

事件当前阶段  3

==========================================

事件发生目标:     内层容器

正在侦听事件的当前目标 外部容器

事件当前阶段  3

==========================================

F  点击中部矩形触发事件如下:

事件发生目标:     中间容器

正在侦听事件的当前目标 中间容器

事件当前阶段  2

==========================================

事件发生目标:     中间容器

正在侦听事件的当前目标 外部容器

事件当前阶段  3

==========================================

F  点击外部矩形触发事件如下:

事件发生目标:     外部容器

正在侦听事件的当前目标 外部容器

事件当前阶段  2

==========================================

 

[1].   DOM Level 3 Events. Document Object Model (DOM) Level 3 Events Specification, http://www.w3.org/TR/DOM-Level-3-Events/#dom-events

[2].   ActionScript3殿堂之路