http://www.ibm.com/developerworks/cn/web/1011_simq_flexlifecycle/?ca=drs-tp4608
Flex 组件生命周期概述
书归正传,下面我们就来介绍 Flex 的生命周期。
首先,Flex 组建的生命周期是什么?它是指 Flex 框架如何同每一个 Flex 组件进行交互,通过什么方法来来控制 Flex 组件的产生、刷新和销毁,以及各个组件又是如何和外界进行通讯的。概括地说,Flex 组件的生命周期可以总结为两个模式、三个时期、四个方法、五个事件、七个阶段。
同客观世界的物质一样,Flex 组件也有着从出生到销毁的三个时期:初生阶段、生长阶段、销毁阶段。每个阶段又会经历若干个重要步骤包括:构造、准备、装配、验证、提交、交互、拆卸和回收。其中,有些步骤组件在一生中只会经历一次,有的则会伴随生命周期重复若干。这些步骤会通过四个重要方法负责实现。那么 Flex 组件如何通知 Flex Engineer 当前处于哪个步骤,又如何告诉变成开发人员当前的状态如何呢?他会通过派发五个不同的事件来进行内外交互;并通过验证 - 提交模式(invitation-validation pattern)来响应更新,从而实现了一个延迟渲染的松耦合的事件模型。
为了能够更加容易理解一下的讲解,让我们来举例说明,我们来从 Flex 组件的基类 UIComponent 出发创建一个简单的图片查看器 ImageViewer(如下图 3 所示), 然后以此为例分别讲解各个时期里,Flex 框架对该组建都做了什么。
图 3. 一个简单的 Flex 组件 ImageViewer
回页首
出生时期
组件的出生阶段包括三个步骤:构造,配置,装配和初始化。(分别对应了 4 个 protected 方法,constructor、createChildren,commitProperties,measure 和 updatedisplayList)
构造函数(Constructor)
编程建议一: 通常对组件本身(并不是组件子元素)的事件监听器会在构造函数里注册。 我们通常也不再构造函数里添加子元素 , 在下面的篇章里会讲解我们为什么不这样做。
首先组件从构造函数(construct)开始出生。当您使用 new 操作符(如清单 1 所示)或者在 mxml 里声明一个组件的时候,构造函数就被调用了。通过调用构造函数,一个组件类的实例被创建在了内存里。但是仅此而已。因为他是组件生命周期的最早部分,此时组件中的子元素还没有被创建,属性值也不明,所以,通常我们在这一阶段里所做的事情很少;但是他也是一个为组件添加时间监听器的好地方,因为一个组件的构造函数会且只会被调用一次。
清单 1. 构造函数被调用
private
var myImageView : ImageViewer = new ImageViewer();
配置阶段(get & set)
初生阶段的第二个步骤是配置。在组件中定义的一些特性(properties)会在这个阶段通过 set 方法被赋值。但是请注意,此时组件的子元素仍然没有被创建,所以如果组件的某个属性的 set 方法里涉及到了对子元素或者其属性的操作的话,请格外留意。如清单 2 所示,假设我们把 ImageViewer组件放到一个 Panel 容器里 (),此时代码的执行顺序如下:
清单 2. 配置阶段的执行顺序
输出顺序:
Panel : constructor
Panel.with : setter
ImageViewer Calendar : constructor
ImageViewer.imageHeight :setter
所以 Adobe 建议开发人员在配置阶段只缓存特性值,而把业务逻辑推迟到到以后的验证阶段(参见清单 3)。这就是之前提到的验证 - 提交模式(invitation-validation pattern),关于这一概念我们会在下面的章节做详细说明。
编程建议二: 使用组件的 set 方法是缓存特性值。将真正的业务逻辑推迟到提交阶段。
清单 3. 例子 ImageViewer 的 set imageHeight() 方法
public
function
set imageHeight(value:Number): void
{
if (_imageHeight != value)
{
_imageHeight = value;
imageHeightChanged = true ;
this .invalidateDisplayList()
dispatchEvent( new Event( "imageHeightChanged" ));
}
}
装配阶段(addChild)
组件被构造以及赋值之后,并不会自动进入整个生命周期的循环,他必须经过装配时期,及组件自身装配到显示列表(Display List)上。组件只有通过 addChild 或者 addChildAt 方法被转配到 Display List 上,才会依次进入到以下的生命周期时期,否则得话,以后的步骤和方法都不会被调用。为此我们可以做这样一个实验。我们在组件代码里添加 trace(清单 4),然后再分别执行应用程序 Test1.mxml(清单 5)和 Test1.mxml(清单 6),再来 对两个输出。
清单 4. 验证装配阶段 ImageViewer 组件的执行顺序
package dw.flex.sample
{
import mx.core.UIComponent;
public
class ImageViewer extends UIComponent
{
public
function ImageViewer()
{
trace( "constructor" );
super();
}
override
protected
function createChildren(): void
{
trace( "createChildren" );
super.createChildren()
}
override
protected function commitProperties(): void
{
trace( "commitProperties" );
super.createChildren()
}
override
protected function measure(): void
{
trace( "measure" );
super.createChildren()
}
override
protected function updateDisplayList(w:Number, h:Number): void
{
trace( "createChildren" );
super.updateDisplayList(w,h)
}
}
}
清单 5. 应用程序 Test1 及其输出
应用程序 Test1.mxml
import dw.flex.sample.ImageViewer;
private
var myImageView : ImageViewer = new ImageViewer();
]]>
输出:
constructor
编程建议三: 推迟组件的装配(使用 addChild 方法将组建添加到父节点上),除非您真的需要他。
清单 6. 应用程序 Test2 及其输出
应用程序 Test2.mxml
import dw.flex.sample.ImageViewer;
private
var myImageView : ImageViewer = new ImageViewer();
override
protected
function createChildren(): void
{
super.createChildren();
addChild(myImageView);
}
]]>
输出:
constructor
createChildren
commitProperties
measure
createChildren
初始化阶段(Initialization)
初始化阶段发生在装配之后,这个阶段包含了 3 个子阶段,派发 3 个事件。
组件派发 Preinitialize 事件。这个阶段意味着组件已经被添加到了显示列表(DisplayList)上。
调用 protected 方法 createChildren() 来生成子元素。在这个阶段里,您可以覆盖这个方法添加需要的子元素。
派发 initialize 事件。这意味着组件及其所有的子元素都已经被创建、配置装备到了 DisplayList 上。
进入第一次验证和提交阶段。Flex 框架会通过 layoutManager 引擎来依次调用组件的三个验证方法 invalidateProperties,invalidateSize() 和 invalidateDisplayList(). 以及其其分别对应的三个提交方法 CommitProperties(),measure() 和 updateDisplayList()。关于这三组方法究竟都是做什么的,我们会在以下的验证阶段详细介绍。
最后配发 creationComplete 事件。
至此,组件的初始化阶段完成,同时也意味着 Flex 组件的出生时期结束了。总结以上个步骤,我们用图 4 来标注组件出生时期的流程图。
编程提示四: 覆盖 createChildren 方法来添加子节点,此方法会且只会被执行一次。 尽量把创建动态形式和数据驱动(data-driven)形式的子元素推迟到 CommitProperties() 方法里添加或者执行。
图 4. 组件出生时前交互流程
回页首
生活时期
组件经过出生时期之后就进入了生活时期,这个时期意味着组件可以被看见,被操作,甚至被删除。并且作为一个成熟的个体,组件可以和外界进行交互,对更新请求进行响应。Flex 组件使用 Invalidation – Validation 模式来响应更新请求。
验证 - 提交模式(Invalidation -Validation Pattern)
Flex 的生命周期是建立的帧模型基础之上的,所以同样遵循着先执行脚本后渲染屏幕的规则。为了达到流畅的视觉体验,我们通常期望能够尽量减少代码执行的消耗,而给画面渲染留下充足的时间,为了实现这点 Flex 使用了 Invalidation – Validation 模式,来实现资源的有效利用。
图 5. Invalidation-Validation 模式
Invalidation – Validation 模式即验证提交 - 模式提供了一个低耦合的处理过程,将数据的改变(旧数据的实效)和数据的处理(对新数据的重新使用)区分开来,这样做的好处是:
只在有屏幕刷新需求的时候的时候进行相应数据操作和计算;
并且避免了不必要的重复渲染。
以清单 7 为例,通过 Invalidation – Validation 模式,第二行的代码的结果永远不会生效,即不会也从来没有显示到屏幕上。
清单 7. 相同属性被多次赋值的例子
myImageView.height = 50;
myImageView. height = 100;
那 Flex 框架是如何实现这一模式的呢?让我们来看一下 Flex 是如何将这一模式应用到组件上的。
首先我们来看一下 Flex 可视化组件基类 UIComponent 是如和处理 set width() 方法的。
清单 8. UIComponent 的 set width ()方法
public function set width(value:Number):void {
if(explicitWidth != value) {
explicitWidth = value;
invalidateSize();
}
// 其他代码省略
}
从清单 8 我们可以看出,UIComponent 在首先会去判断属性赋值是否改变,从而避免重复赋值带来的消耗,然后调用 protected 方法 invalidateSize()。
invalidateSize()是 UIComponent 提供的验证方法之一,它用来校验与组件尺寸大小相关的属性,当此方法被调用后,Flex 组件引擎 LayoutManager 会首先检查该属性的更新请求是否已经调用过 invalidateSize()方法,如果调用过,说明此类型的更改请求已经记录在案,无需赘述。如果没有,则会将此次请求记录到 LayoutManager 校验队列上进行等待。那么对 width 属性值的更新什么时候生效呢? LayoutManager 会在监听到下一次 RENDER 事件派发的时候,将组件推送到提交阶段。
在提交阶段,LayoutManager 得到更新请求队列,调用 commitProperties()方法使得属性 width 的最新值(假设记录在了 _width 私有变量上)得以生效。Flex 就是通过这种属性值延迟生效的方法来保证每次用于渲染画面的请求都是最新的,从而避免了不必要的资源浪费。
因此,在我们的例子 ImageViewer 里,我们也使用了类似机制。(如清单 9 所示),只不过这里我们调用的是 invalidateProterties()方法。与 invalidateSize()类似,组件的 scale 属性值会在提交阶段通过调用 commitProperties()方法生效。
清单 9. ImageViewer 的 set scale 方法
public
function
set scale(value : Number): void
{
if (_scale != value)
{
_scale = value;
scaleChanged = true ;
invalidateProperties();
dispatchEvent( new Event( "scaleChanged" ));
}
}
验证阶段(Invalidation)
验证阶段是组件在生活要经历的第一步,具体地说是响应更新请求的第一步。作为 invalidation-validation 循环的一份子他会在组件的生命周期中多次执行。Flex 组件基类 UIcomponent 提供了三种验证方法:invalidateProperties()、invalidateSize()和 invalidateDisplayList() 来分别响应组件对值相关,布局相关和显示相关属性的更新请求。
组件会通过两种方式进入到验证阶段:
第一种方式是在初始化阶段,通过父节点调用 childrenCreated() 方法,组件进入第一次 Invalidation – validation 循环。这一次,三种验证方法都会被自动调用以确保组件初始化成功。这类调用在组件的生命周期内只会经历一次。
第二种方式是响应更新请求。组件的任何一方面特性发生更新请求后,都会进入到验证阶段。而此时,用户需要根据特性的不同类别来自行决定调用哪种验证方法。还是以清单 9 为例,通过调用 invalidateProperties()方法,确保了在接下来的提交阶段,更新会对值计算生效。
在这个时期,我们通常不去做太多的事情,因为此时只是验证阶段,更新并不生效。我们所要做的只是记录此次更新,并且等待进入提交阶段。
编程提示五: 响应组件更新请求时,调用三种验证方(invalidateProperties()、invalidateSize()、invalidateDisplayList()),而不要显试调用提交方法(commitProperties(),measure()和 updateDisplayList())。Flex 框架会在最合适的时候自行调用提交方法。
提交阶段(Validation)
通过以上的介绍,我们已经很清楚组件更新会在提交阶段实际生效。和验证阶段的随时更新随时校验不同,Flex 框架为提交阶段设计了线性执行的顺序,将三个提交方法分为了三个阶段。顺序调用三个提交方法:commitProperties(),measure()和 updateDisplayList()。这样做的目的和每个方法的功能有关。commitProperties()方法主要负责属性值的计算和生效,因此首先执行,因为这些自己算过的值可能用于布局,也可能用于显示。这里也可以用来移除不需要的子节点,但是您需要通过标志位来判断子节点是否存在。
measure()方法用于根据组件的内容大小计算组件尺寸。由于尺寸的计算是自下而上的,所组件子元素的大小改变都会隐试的触发此方法。
UpdatedisplayList() 方法用来更新显示列表(Display List)的布局以及渲染。组件的布局是一个自上而下的过程,所以在这个方法里您需要对不仅仅是组件设置布局属性,而且也要对子元素进行相应的调整。这里也是使用 Flash 绘图 API 的好地方。
这里,我们所要做的就是覆盖(override)这几个方法,在正确的时间、正确的地方做正确的事。
编程提示六: 通过挑选并重写合适的提交方法,您可以把复杂的逻辑处理延迟到提交阶段通过设置标志位,可以进一步优化提交阶段的更新性能。
交互阶段(Interaction)
交互严格地说组件生命周期中的某种行为,他是促使组件更新,推动验证 - 提交循环的动力。
Flex 使用事件驱动的交互模式,来实现一种完全松耦合的体系结构。简单地说,任何组件如果想要告诉外界当前发生了什么事或者即将发什么事的话,他可以派发一个事件,那么在该类事件可及范围内的任何组件都可以通过注册该事件的监听器的方式来对此类事件进行相应。关于 Flex 在事件机制处理方面信息的由于超出了本文的范围,这里不再多讲,感兴趣的读者可以关注后续教程《探索 Flex 的事件机制》,或者阅读资料部分的相应文档。
一般来说,组件的交互行为有以下几种:
用户同组件的交互,比如说:输入数据,缩放大小等等。
通过派发和响应事件。
应用(Application)或者父节点(parent)与组件的交互。比如说 ItemRenderer 实例和宿主对象之间的 data 传递。(关于 ItemRenderer 机制和实践讲解,也会有后续教程加以探讨)
回页首
销毁时期
任何事物都会有一个归宿,Flex 组件也不例外。当某个组件不再需要的时候,我们需要把他销毁。(这听起来有点残酷,但是我们不得不这么做)
拆卸阶段(removeChild)
销毁组件的一种方法是通过调用组件父亲节点(parent)的 removeChild()方法,该方法会将他从显示列表(Display List)上拆卸掉,并且检查是否存在对此组件的引用,如果没有的话,组件会被标记为“可以回收”,这预示着组件进入到了回收阶段,并且可以被 AVM 垃圾回收。
回收阶段(GC)
刚才我么提到了通过 removeChild()方法将组建拆卸掉以后,组件可以被垃圾回收。这意味着该组件的实例会被从内存中完全删除掉,并且释放资源。但是请注意,垃圾回收并不一定在此刻马上发生,AVM 有着自己的垃圾回收时间。因此这个打了标签的组件需要等待回收时刻的到来。
拆卸阶段并不是组件销毁的必经阶段。当某个组件被拆卸掉之后,如果该组件包含了子组件,而他们都不存在外界引用的话,所有的元素都会被标记为“可以回收”,也就是说该系统中的子组件并不需要进入到拆卸阶段,也可以在回收时刻到来的时候被 AVM 回收掉。那么开发人员所需要注意的就是,在这个时刻发生之前,将引用去除掉。
回页首
实际编程中的应用
到目前为止我们学习的多是 Flex 生命周期的理论。学习理论的最终目的就是知道编程实践,现在我们来看一下生命周期是如何在例子 ImageViewer 里是被贯彻执行的。
关于 createChildren()方法
清单 10 显示了组件 ImageViewer 的 createChildren() 方法。正如大家注意的那样,在创建每一个子节点的时候,首先判断该节点是否存在是一个很好的习惯,这样可以防止子节点在某种情况下已被实例化。是的,这种情况是可能发生的。以清单 11 里的 Table 类为例, Table 里的子节点 row 有一个 set()方法,而我们从“初生阶段”里知道,装配阶段(set 方法调用)要早于初始化阶段,那么请大家思考一下清单 12 的执行结果会是如何。
清单 10. ImageViewer 的 createChildren() 方法
override
protected
function createChildren(): void
{
super.createChildren();
if (! this.border){
createBorder();
}
if (!controlBar){
controlBar = new UIComponent();
}
if (!zoomInButton){
zoomInButton = new Button();
zoomInButton.label = "+" ;
zoomInButton.addEventListener(MouseEvent.CLICK,
zoomInButtonClickHandler);
controlBar.addChild(zoomInButton);
}
if (!zoomOutButton){
zoomOutButton = new Button();
zoomOutButton.label = "-" ;
zoomOutButton.addEventListener(MouseEvent.CLICK,
zoomOutButtonClickHandler);
controlBar.addChild(zoomOutButton);
}
// added controlBar the last time
addChild(controlBar);
}
清单 11. Table 类的例子
public
class Table extends TableBase
{
private
var _row : Row;
public
function
getrow():Row
{
return
this._row;
}
public
function
setrow(r : Row): void
{
_row = r;
this.invalidateDisplayList();
}
public
function Table()
{
super();
}
override
protected
function createChildren(): void
{
super.createChildren();
if (!_row){
_row = new Row();
}
this.addChild(_row);
}
清单 12. Table 和 Row 的初始化顺序
var t : Table = new Table();
t.row = new Row();
addChild(t);
我们也看到 controlBar 在该方法的末尾才被添加到 Display List 上,正如之前提到的那样,我们只在需要的时候装配他。同时,此时也是为子节点添加监听器的好地方。
关于 commitProperties()方法
下面我们来看一下 ImageViewer 组件在 commitProperties()里都做了什么(如清单 13 所示)。
CommitProperties()是验证方法 invalidateProperties()所对应的提交方法,也是初始化阶段会被调用的第一个提交方法。他的目的是使得任何通过 set 方法提交的数值更改生效。所以您可以看到在清单 9 的 set scale()方法里,按照 invalidation-validation 模式,我们调用了 invalidateProperties()方法从而将值的生效延迟到了 commitProperties()里,并且为了做到 “局部更新”,我们使用了标志位 scaleChanged。
另外,这里这里一个奇怪的地方,那就是为什么在 commitProperties()会有添加子节点的操作呢?这也是我们要特意举例说明的一个地方。
对于有些子节点,他的诞生可能是和某些属性值相关联的,也就是我们在编程提示四里提到的动态创建或者数据驱动的子节点。这些子节点,由于他们并不是随着组件的产生而产生,而是要受属性值的变化而产生或者变化,甚至在某些情况下,根本就不需要存在。所以我们应该在值的提交阶段,也就是 commitProperties()方法调用的时候,当新值真正生效的时候再去创建它。
清单 13. ImageViewer 的 commitProperties()方法
override
protected
function commitProperties(): void
{
super.commitProperties();
if (sourceChanged){
if (! this.image){
image = new Image();
this.addChild(image);
image.source = this._source;
} else {
image.source = this._source;
}
sourceChanged = false ;
}
if (scaleChanged){
this.imageWidth = this.imageWidth * this.scale;
this.imageHeight = this.imageHeight *+ this.scale;
scaleChanged = false ;
}
}
关于 measure() 方法
measure()方法是组件自动计算尺寸大小的地方,在例子 ImageViewer 的 measure()方法里(如清单 14 所示),我们做的事很少。这是因为,我们 ImageViewer 被设计为:需要显式指定大小(当然这里只是为了举例方便,您可以根据需要,制作可以自动度量大小的组件)。即在应用时设置 width 和 height 值。如清单 15 所示:
在这种情况家,measure()方法不会被调用。所以需要提请读者注意的就是,一旦您的组件在组装阶段被设置了 with 和 height 属性值,那么请不要期望在 measure 里会执行您的业务逻辑。
清单 14. ImageViewer 的 measure() 方法
override
protected
function measure(): void
{
super.measure();
measuredWidth = Math.max(image.width, controlBar.getExplicitOrMeasuredWidth()) ;
measuredHeight = image.height + controlBar.getExplicitOrMeasuredHeight();
measuredMinWidth = measuredWidth;
measuredMinHeight = measuredHeight;
}
清单 15. 使用 ImageViewer 的应用 Sample.mxml
关于 updateDisplayList ()方法
updateDisplayList()方法用于对组件内容进行布局以及渲染,一切屏幕上可见的内容都需要在这里被处理,所以 updateDisplayList()可以说是最繁忙的一个提交方法,他所包含的实现可以非常多。清单 16 中,我们省略了部分代码。只留下了需要讲解的部分。
在 measure()方法里我们可以获取 Flex 自动计算的尺寸(如果被调用的话),这些数据需要在 updateDisplayList() 方法里被应用处理,所以我们会大量使用 setActualSize()方法,特别是子元素比较多的时候。
作为提交方法,updateDisplayList()的最重要的职责之一就相对应的 invalidateDisplayList()方法的更新请求进行响应。比如说清单 16 里方法就对清单 17 里的 set imageWidth()方法进行了相应。并且就像在 commitProperties()部分里介绍的那样,这里同样使用了“标志位”方法来进行“局部跟新”。
局部更新是普遍应用于提交方法里的一种技巧,因为我们知道这三个提交方法是公用的,任何更新的提交都会在这几个方法里被处理。而每次更新都可能只是局部的更改,所以是当地使用标志位方法进行局部更新可以有效地优化代码的执行。
最后要提到的是,渲染屏幕的职责也决定了 updateDisplayList()方法是调用 Flash Player 绘图 API 的好地方。所以我们在清单 16 中特意使用了 drawRect()方法为图片家了一个边框。
清单 16. ImageViewer 的 updateDisplayList () 方法。
override
protected
function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number): void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
// 省略部分代码
zoomInButton.setActualSize(50, 20);
zoomOutButton.setActualSize(50, 20);
var tmpx : Number = 20;
controlBar.setActualSize(tmpx, 20);
controlBar.move(0, unscaledHeight-25);
zoomInButton.move(tmpx, 0);
tmpx +=60;
zoomOutButton.move(tmpx, 0);
if (imageHeightChanged ||imageWidthChanged){
image.width = this.imageWidth;//w;
image.height = this.imageHeight;//h - 20;
var tempX : Number = x+ (w-imageWidth) * 0.5;
var tempY : Number = y + (h-imageHeight) * 0.5;
image.x = tempX;
image.y = tempY;
imageHeightChanged = false ;
imageWidthChanged = false ;
var g:Graphics = graphics;
g.clear();
g.beginFill(0x6F7777);
g.drawRect(tempX-1, tempY-1, imageWidth+2, imageHeight+2);
g.endFill();
}
}
清单 17. ImageViewer 的 set imageWidth () 方法。
public
function
set imageWidth(value:Number): void
{
if (_imageWidth != value)
{
_imageWidth = value;
imageWidthChanged = true ;
this.invalidateDisplayList()
dispatchEvent( new Event( "imageWidthChanged" ));
}
}
回页首
结束语
本文对 Flex 生命周期的各个阶段进行了分类和讲解,根据各个时期的特点提出了一些编程方面的提示,并且通过实例分析将这些编程提示应用到实践中,希望对您的 Flex 开发工作有所帮助。