这篇文章是拷贝过来的,读完确实让我对Flex了解的更通透了,文章比较长,请耐心看完。
英文原文:?Flex 4 Gumbo DOM Tree API - Functional and Design Specification
翻译的原创链接: ?http://www.smithfox.com/?e=36??转载请注明, 文中如果有什么错误的地方或是讲的不清楚的地方,欢迎大家留言.
这是一篇难得的Flex功能和架构技术SPEC,耐心看完绝对有收获.
为了振作你看这个文章的兴趣,假设你应聘Flex工作被问到了下面的几个问题:
1. Flex中owner和parent有什么区别?
2. addChild和addElement两套函数有什么不同,(不是指怎么使用不同,而是指框架内部的设计有什么不同)?
3. <s:Rect>是GraphicElement吗,他们为什么可以放在<s:Group>内?
4.?SkinnableComponent,?SkinnableContainer,Group,DataGroup以及SkinnableDataContainer有什么区别?
5. 最关键的是: 你知道smithfox吗?(哈哈)
目的
在Flex 4中有许多DOM(Document Object Model)树。他们到底是怎么组织和呈现的?
定义
图形元素(graphic element)?- 就象是矩形,路径,或是图片. 这些元素不是DisplayObject的子类; 但是它们还是需要一个DisplayObject来渲染到屏幕. (smithfox注: "多个图形元素可以只用一个DisplayObject来渲染")
视觉元素(visual element)?- (英文有时简称为 - "element"). 可以是一个halo组件,或是一个gumbo组件,或是一个图形元素. 视觉元素实现了接口?IVisualElement.
数据项?(英文有时简称为 - "item") - 本质上Flex中的任何事物都可以被看着数据项. 通常是指非可视化项,比如 String,Number,XMLNode,等等. 一个视觉元素也能作为数据项 -- 这要看他是怎么被看待的.
组件树?- 组件树表现了MXML文档结构. 举个简单例子,一个?Panel?包含了一个?Label. 这个例子中,Panel?和?Label?都在组件树中,但是?Panel的皮肤却不是.
布局树?- 布局树呈现了运行时的布局. 在这个树中,父亲负责呈现和布局对象,孩子则是被布局的视觉元素. ?举个简单例子,sans-serif">Label.??这个例子中,?Label?都在布局树中,?同样Panel的皮肤和皮肤中的contentGroup也是.
显示树?- Flash 底层 DisplayObject 树.
本文中的全部图的图例如下:

背景:
当你用MXML创建应用程序时,幕后发生了许多的事情,会将MXML转换成Flash显示对象. 后台有三个主要因素: 皮肤,项渲染和显示对象sharing. 前两个对开发人员是非常重要的概念; 最后一个只需要框架开发人员关注,但仍然比较重要.
皮肤:
当你初始化一个?Button,其实创建了不止一个对象. 例如:
?在布局树中的结果是:

(注: TextBox 已经更名为 Label)
一个皮肤文件被实例化了,并且加入到Button的显示列表中.Button的皮肤文件如下:
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" minWidth="23" minHeight="23">
<fx:Metadata>
[HostComponent("mx.components.Button")]
</fx:Metadata>
<s:states>
<s:State name="up" />
<s:State name="over" />
<s:State name="down" />
<s:State name="disabled" />
</s:states>
<!-- background -->
<s:Rect left="0" right="0" top="0" bottom="0"
width="70" height="23"
radiusX="2" radiusY="2">
<s:stroke>
<s:SolidColorStroke color="0x5380D0" color.disabled="0xA9C0E8" />
</s:stroke>
<s:fill>
<s:SolidColor color="0xFFFFFF" color.over="0xEBF4FF" color.down="0xDEEBFF" />
</s:fill>
</s:Rect>
<!-- label -->
<s:Label id="labelDisplay" />
</s:Skin>
尽管Button看上去是一个叶子结点,但因为皮肤的存在,实际上他包含了孩子. 为访问这些元素,所有SkinnableComponent对象都定义了skin属性. 这样就可以通过Button.Skin实例来访问Rectangle?和Label. 如要访问Label,你可以写成:myButton.skin.getElementAt(2)或是?myButton.skin.labelDisplay.由于labelDisplay是?Button?的 skin part,所以你可可以直接写成?myButton.labelDisplay.
同样的原则也一样适用在SkinnableContainer.?SkinnableContainer是容器所以天然就有孩子,但同时他们也是SkinnableComponent,所以也有一个皮肤以及来自皮肤的孩子.
(smithfox注: SkinnableContainer的确是继承自SkinableComponent,见图)

还是以Panel为例:
<s:Panel>
<s:Button />
<s:Label />
<s:CheckBox />
</s:Panel>
panel 有三个孩子: 一个button,一个label,和一个checkbox. 用定义在SkinnableContainer上的content APIs可以访问他们. 这些content?APIs很像flash?DisplayObjectContainer?的 APIs,包括addElement(),addElementAt(),getElementAt(),getElementIndex(),等等.... 所有方法的完整列表在稍后文档中列出.
因为 panel有3个孩子,它的组件树象这样:

(注: TextBox 已经更名为 Label)
但是,这只是组件树. 因为皮肤的原因,sans-serif">Panel真正布局树是这样的:

(注: TextBox 已经更名为 Label)
在上面这张图上有许多箭头. 需要注意的有:
- Panel的组件孩子有: button,label,和checkbox.
- button,和checkbox的组件父亲(owner?属性) 是?Panel.
- button,and checkbox的布局父亲 (parent?属性)?是 Panel皮肤的contentGroup.
这意味着即使看上去Panel的孩子应该是一个button,和一个checkbox; 但实际上真正的孩子是一个panel皮肤实例. button,和 checkbox 向下变成了皮肤中contentGroup的孩子. 有几种方法可以访问panel中的Button:?myPanel.getElementAt(0)?or?myPanel.contentGroup.getElementAt(0)?or?myPanel.skin.contentGroup.getElementAt(0).
所有?SkinnableComponent 都有?skin?属性. 在?SkinnableContainer中组件的孩子实际上下推成为skin的?contentGroup的孩子.?组件树?指向编译自MXML的语义树.?Panel?例子中,只包括Panel?和他的孩子: 一个 button,一个?label,和一个checkbox. 由于皮肤,?布局树?是布局系统所实际看到的树.contentGroup的孩子).
布局树无需和所见的Flash显示列表有什么相关性. 这是因为?GraphicElement?不是天然的显示对象. 因为考虑效率的原因,他们最小化了显示对象数目(smithfox注: 多个GraphicElement可以在一个DisplayObject上渲染,这样DisplayObject的总数就可以大大减少).
(smithfox注: GraphicElement是spark的类,确实是少有继承层次非常少的对象,如图:)

IVisualElementContainer?定义了content?APIs. 在Spark中,?Skin,sans-serif; line-height:19px; font-size:14px; text-align:left">Group,和?SkinnableContainer?实现了这个接口,持有着可视化元素. 为保持一致性,MX的?Container?也实现了这个接口,不过只是对addChild(),numChildren,等函数的封装....
这个接口使访问树变得容易了. 本质上,这个接口为容器对外暴露有它哪些孩子提供了方法. 例如,sans-serif">FocusManager就是这样. 该接口使得 focus manager不依赖于Group?或是其它 Spark代码(除了这个接口),MX也不必增加太多代码. 我们讨论过要不要增加这些变异的(mutation) APIs,要不要MX也实现这些接口,但我们认为这将有助有开发人员(框架开发人员) 实现所有容器(MX和Spark). 当我们看 DataGroup and SkinnableDataContainer 代码时,你会发现他们并没有实现IVisualElementContainer接口,尽管DataGroup有几个相似的 "只读的" 方法,比如?numElements?和?getElementAt().
(smithfox注: 从Spark最终SDK中的代码可以验证,如图)


IVisualElementContainer?持有?IVisualElements.?IVisualElement?是可视化元素的一个新接口. 它包含了一些必要的属性和方法以使容器可以增加element. 他继承自?ILayoutElement?并增加了一些其它属性.
(smithfox注:?IVisualElement接口为什么是放在mx.core包内,确实有点怪,但这是事实,如图)

视觉元素的parent,也就是容器,直接负责布局. 视觉元素的owner是视觉元素的逻辑持有组件. 如果一个 Button在一个SkinnableContainer里,它的parent是contentGroup而它的owner 是这个?SkinnableContainer.
请注意 ?parent?和?owner?属性类型是?DisplayObjectContainer?而不是?IVisualElementContainer. 这是因为在MX内,这些属性就是?
DisplayObjectContainer. 此外,因为?parent?属性是继承自 Flash的?DisplayObject,我们无法改变他. 我们曾讨论过为这个属性起个新名字,但最后我们认为这样不值得.
(smithfox注: DisplayObjectContainer是flash.display.Sprite的父类)
MX 组件
MX 组件和有上面有着相同的概念,但是大部分隐藏在后台. Spark组件则因为皮肤化就变得更加透明.
一个MX button有一个孩子,就是TextField. 这个孩子是直接通过addChild() (没有皮肤)方法加到Button的. 例如,这个Button的TextField就是Button的孩子. 所以如果你查看Button的孩子,他将返回给你这个TextField. 如果你问这个TextField父亲,他将返回这个Button.
在Spark中,一个Button只有一个孩子,皮肤对象. 皮肤对象包含了一个Label. 如果你问Button的显示对象孩子,它将告诉你它有一个孩子:皮肤. 如果你想确认Button皮肤的孩子,你应该调用皮肤对象中的方法.
容器有些难懂,它包括了组件孩子和皮肤孩子. 在MX中,Panel的显示列表包含了皮肤孩子和一个叫"contentPane"的组件孩子. panel的所有组件孩子都放到这个contentPane. 这和Spark非常象; 然而,在MX中对开发人员隐藏了太多细节. 如果你问Panel的显示列表孩子,它其实对你撒谎了,它返回你这个contentPane孩子(Panel的组件孩子). 为访问皮肤孩子,可以通过rawChildren?属性返回孩子列表. 如果你问Panel的组件孩子的它的父亲是谁,它会告诉你是这个panel,但实际上他的父亲应该是contentPane.
在Spark中,sans-serif">IVisualElementContainer接口可以让你访问孩子. 这也是Spark组件宣布谁是他的可视化孩子的方式.Group?和?SkinnableContainer?都实现了这个接口. 另外,MX的?Container?也实现了这个接口. 但那只是对显示列表APIs的一种封装,sans-serif">IVisualElementContainer?提供了唯一的,一致的访问容器孩子的方法.
在Spark中,sans-serif">SkinnableContainer 仍然有DisplayList API(smithfox注: 就是在Flex 3中的操作children的函数,比如addChild). 但是,但是如果你想试图通过这些API操作 DisplayList,我们将抛出一个运行时异常. 当你访问?numChildren?或是?getChildAt()函数,不像在MX中,Spark会如实地返回他的显示列表. 当你调用SkinnableContainer的 "content API" (numElements,getElementAt()) ?,它将返回它的组件孩子 (contentGroup的实际的所有孩子). 要访问皮肤孩子 (就象MX组件中的"rawChildren"),你需要调用skin对象的方法. 当你问Panel组件的孩子问谁是它的parent,它会返回contentGroup?(不象MX返回这个Panel). 但是有另外一个属性会返回Panel,那就是owner. owner属性MX也有,但是在MX中和它parent属性返回的是一样. 在Spark中,owner 和 parent则指向了不同的对象.
数据项
在Spark中,有两个主要的容器类型: 一个容纳可视化元素,另一个容纳数据项.?DataGroup?和?SkinnableDataContainer?用来容纳数据项.?SkinnableContainer?用来容纳可视化元素. 一个数据容器能容纳任何东西,但特别是用来容纳非可视元素 (比如.-真正的数值). 有关数据容器重要的一点是它们支持 项渲染,就是将数据项转换为可视元素.
项渲染
DataGroup?有能力将随意的非可视化元素呈现到屏幕. 因此,项渲染器正好可以加到布局树中. 某些情况下,甚至于可视化元素,sans-serif">UIComponents?和?GraphicElements,也被包装成项渲染器. 为向开发人员展现这个设计思路,我们考虑一下以下几个可选方案:
- DataGroup 和 SkinnableDataContainer 设计成叶子节点,他们的实际可视化孩子不能被访问
- DataGroup 和 SkinnableDataContainer 实现IVisualElementContainer接口. 当问屏幕上有几个可视化元素时,我们只返回当前屏幕正在被渲染的那些元素. Mutation APIs RTE(RuntimeException).
- DataGroup 和 SkinnableDataContainer 实现 我们决定向DataGroup增加?"只读"的 element APIs,象numElements,sans-serif">getElementAt(),和?getElementIndex(). 还有另一个API,sans-serif">getItemIndicesInView()决定哪些数据项在屏幕显示.
象MX一样,项渲染器的?owner?属性总是和组件的 owner属性是一样的. 项渲染器的?parent?属性负责渲染.
这两个图显示了项渲染的运行.

你会注意到DataGroup?在组件树中没有孩子. 这是因为它被看着是渲染数据的叶子节点. 下图是DataGroup的布局树例子:

(注: TextBox 已更名为 Label)
上面例子中,字符串不是一个可视化的元素并且需要一个项渲染器. 创建一个项渲染器包装这个字符串对象. 它的owner属性就是DataGroup. 因为设置了一个 itemRendererFunction 对象,所以 Employee Object 和其它的字符串一样都会得到处理.
用例:
开发人员通常只和组件树打交道. 布局和效果就像FocusManager一样和布局树打交道. 只有像Group的 DisplayObject的sharing code这样的底层的代码才和显示树打交道.
API 说明
public interface IVisualElementContainer {
public function get numElements():int;
public function getElementAt(index:int):IVisualElement;
public function getElementIndex(element:IVisualElement):int;
public function addElement(element:IVisualElement):IVisualElement;
public function addElementAt(element:IVisualElement,index:int):IVisualElement;
public function removeElement(element:IVisualElement):IVisualElement;
public function removeElementAt(index:int):IVisualElement;
public function setElementIndex(element:IVisualElement,index:int):void;
public function swapElements(element1:IVisualElement,element2:IVisualElement):void;
public function swapElementsAt(index1:int,index2:int):void;
}
public interface IVisualElement extends ILayoutElement {
owner:DisplayObjectContainer;
parent:DisplayObjectContainer;
...other stuff not discussed here...
}
public class UIComponent implements IVisualElement {
owner:DisplayObjectContainer;
parent:DisplayObjectContainer;
...other stuff...
}
public class GraphicElement implements IVisualElement {
owner:DisplayObjectContainer;
parent:DisplayObjectContainer;
...other stuff...
}
[DefaultProperty("content")]
public class Group extends GroupBase implements IVisualElementContainer {
[write-only] mxmlContent:Array;
layout:ILayout;
public function get numElements():int;
public function getElementAt(index:int):IVisualElement;
public function getElementIndex(element:IVisualElement):int;
public function addElement(element:IVisualElement):IVisualElement;
public function addElementAt(element:IVisualElement,index2:int):void;
}
public class Skin extends Group {
}
public class SkinnableComponent extends UIComponent {
function get skin():Skin;
[CSS] function set skinClass:Class;
}
[DefaultProperty("content")]
public class SkinnableContainer extends SkinnableContainerBase implements IVisualElementContainer {
[write-only] mxmlContent:Array;
public function get numElements():int;
public function getElementAt(index:int):IVisualElement;
public function getElementIndex(element:IVisualElement):int;
public function addElement(element:IVisualElement):IVisualElement;
public function addElementAt(element:IVisualElement,index2:int):void;
[SkinPart] contentGroup:Group;
}
public class Container extends UIComponent implements IVisualElementContainer {
public function get numElements():int;
public function getElementAt(index:int):IVisualElement;
public function getElementIndex(element:IVisualElement):int;
public function addElement(element:IVisualElement):IVisualElement;
public function addElementAt(element:IVisualElement,index2:int):void;
}
[DefaultProperty("dataProvider")]
public class DataGroup extends UIComponent {
dataProvider:IList;
itemRenderer/itemRendererFunction;
layout:ILayout;
public function get numElements():int;
public function getElementAt(index:int):IVisualElement;
public function getElementIndex(element:IVisualElement):int;
public function getItemIndicesInView():Vector.;
}
[DefaultProperty("dataProvider")]
public class SkinnableDataContainer extends SkinnableContainerBase {
dataProvider:IList;
layout:ILayout;
itemRenderer/itemRendererFunction;
[SkinPart] dataGroup:DataGroup;
}
//遍历这些树的样例代码:
public function walkTree(element:IVisualElement,proc:Function):void
{
proc(element);
if (element is IVisualElementContainer)
{
var visualContainer:IVisualElementContainer = IVisualElementContainer(element);
for (var i:int = 0; i < visualContainer.numElements; i++)
{
walkTree(visualContainer.getElementAt(i));
}
}
}
public function walkLayoutTree(element:IVisualElement,proc:Function):void
{
proc(element);
if (element is SkinnableComponent)
{
var skin:Skin = SkinnableComponent(element).skin;
walkTree(skin);
}
else if (element is IVisualElementContainer)
{
var visualContainer:IVisualElementContainer = IVisualElementContainer(element);
for (var i:int = 0; i < visualContainer.numElements; i++)
{
walkTree(visualContainer.getElementAt(i));
}
}
// expand this to MX and IRawChildrenContainer?
}
public function walkUpTree(element:IVisualElement,proc:Function):void
{
while (element!= null)
{
proc(element);
element = element .owner;
}
}
public function walkUpLayoutTree(element :IVisualElement,proc:Function):void
{
while (element != null)
{
proc(element );
element = element .parent;
}
更多关于 parent/owner
有一种看待parent?属性的方法是: "谁布局我". 如果你是一个DisplayObject,这同时也对应着你的物理显示列表parent. (GraphicElements这里做了一点伪装,因为他们并不是显示对象,但也同一个概念).
owner属性的用途:
- 它能告诉你在组件树(或是SkinnableContainer中的elements)中谁是你的父亲
- 它能告诉项渲染器哪个数据容器负责他们
- 它还用在弹出窗口,象?DateField,它告诉你谁在负责这个弹出窗口.
看待owner属性的方式就是: 一个元素的?owner?指向了负责它的组件.
需要做的一些变化(smithfox注: 这个是Flex 4的设计规格,所以应该是说给adobe开发人员听的)
为GraphicElement增加parent和owner属性. 在适当的地方将这些属性(项渲染器和SkinnableContainer)衔接起来.
建议主要还是创建和实现这些接口.
已经分开了Group和DataGroup. 这就可以按完全独立的规范性工作项目来运作.
最后,还需要做些虚拟化规范相关的工作,以实现怎样呈现这样已经渲染过的元素.
重要的/有争议的 观点:
- 这些不同的DOM树有些让人迷惑,我们需要向Flex开发人员做些明确的解释.
- 我们引入owner?属性是因为这样人们可以遍历逻辑DOM树. 我们引入?parent?属性是因为这样人们可以遍历布局DOM树. 我们考虑过是否不需要parent属性,因为它的类型是DisplayObjectContainer,在将来的某个时候,parent 节点不必一定是DisplayObjectContainers. 但现在还是,sans-serif">parent这个名字比其它名字要适合一些. 如果我们决定使容器变成非DisplayObjects,那时我们可能会贯彻到底,将所有的DisplayObject都变成是可选的l.
- Walking the layout tree requires knowledge of?SkinnableComponent?and?Skin. This means Mustella (or other places) will need to bring these classes in (or treat them as untyped).
- MX也实现IVisualElementContainer接口.
- owner?属性看上去有3个不同的用途.
- Scroller?也实现?IVisualElementContainer?接口以宣布它有一个孩子. 我们考虑过为"decorators(装饰)" 创建一个单独的接口,但我们倾向更通用这样也能处理?HDividedBoxes?. 这些"getter"方法将能在Scroller使用,其它的就抛出运行时异常.
- 我们考虑过在gumbo容器中支持flash原生display objects,但最后还是否决了.
- 我们需要支持新的组件工具包和那些用老的组件工具包制作的swc. 一种解决方案是always link in UIMovieClip and the other FCK classes. 这些新定义的类将会实现IVisualElement?和?IVisualElementContainer接口. 因为这些类是新定义的,它们将会覆盖老版本的基类. 另一个解决方案是只更新组件工具包而不再支持老的scw. 我们需要更多的PM的决定; 但是不管怎么样,这样类是需要更样的.
- 我们同时有多套Flash DisplayObjectContainer APIs,他们分别继承自Group/DataGroup/SkinnableComponent (addChild(),getChildAt(),等). 为处理这个问题,所有mutation(变异的) APIs调用 (addChild(),removeChild(),swapChildren(),等...) 都将抛出运行时异常. 只有允许调用"getters" 类的方法. 我们也试图在正常API?(getChildAt,etc...)调用时也抛出异常,但会有架构方面的问题,比如UIComponent的 removeChildAt 方法就依赖于这些API.?如果这原来是一个优先事项,我们可以在这些方法中都加入运行异常并且提供新的方法,比如 $getChildAt_SkinnableComponent之类. 然后我们在这些新API的基础上改动所有framework的代码. 这样做又有新的问题:?[Child APIs vs. Item APIs].