这篇文章是拷贝过来的,读完确实让我对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的皮肤却不是.
布局树- 布局树呈现了运行时的布局. 在这个树中, 父亲负责呈现和布局对象, 孩子则是被布局的视觉元素. 举个简单例子, 一个Panel包含了一个Label.这个例子中,Panel和Label都在布局树中,同样Panel的皮肤和皮肤中的contentGroup也是.
显示树- Flash 底层 DisplayObject 树.
本文中的全部图的图例如下:
背景:
当你用MXML创建应用程序时, 幕后发生了许多的事情,会将MXML转换成Flash显示对象. 后台有三个主要因素: 皮肤,项渲染和显示对象sharing. 前两个对开发人员是非常重要的概念; 最后一个只需要框架开发人员关注, 但仍然比较重要.
皮肤:
当你初始化一个Button, 其实创建了不止一个对象. 例如:
<s: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可以访问他们. 这些contentAPIs很像flashDisplayObjectContainer的 APIs, 包括addElement(), addElementAt(), getElementAt(), getElementIndex(), 等等.... 所有方法的完整列表在稍后文档中列出.
因为 panel有3个孩子, 它的组件树象这样:
(注: TextBox 已经更名为 Label)
但是, 这只是组件树. 因为皮肤的原因,Panel真正布局树是这样的:
(注: TextBox 已经更名为 Label)
在上面这张图上有许多箭头. 需要注意的有:
- Panel的组件孩子有: button, label, 和checkbox.
- button, label, 和checkbox的组件父亲(owner属性) 是Panel.
- button, label, and checkbox的布局父亲 (parent属性)是 Panel皮肤的contentGroup.
这意味着即使看上去Panel的孩子应该是一个button, 一个label, 和一个checkbox; 但实际上真正的孩子是一个panel皮肤实例. button, label, 和 checkbox 向下变成了皮肤中contentGroup的孩子. 有几种方法可以访问panel中的Button:myPanel.getElementAt(0)ormyPanel.contentGroup.getElementAt(0)ormyPanel.skin.contentGroup.getElementAt(0).
所有SkinnableComponent 都有skin属性. 在SkinnableContainer中组件的孩子实际上下推成为skin的contentGroup的孩子.组件树指向编译自MXML的语义树.Panel例子中, 只包括Panel和他的孩子: 一个 button, 一个label, 和一个checkbox. 由于皮肤,布局树是布局系统所实际看到的树.Panel例子中,包括 这个panel, panel的皮肤, 以及这个皮肤的所有孩子(皮肤中的contentGroup的孩子).
布局树无需和所见的Flash显示列表有什么相关性. 这是因为GraphicElement不是天然的显示对象. 因为考虑效率的原因, 他们最小化了显示对象数目(smithfox注: 多个GraphicElement可以在一个DisplayObject上渲染, 这样DisplayObject的总数就可以大大减少).
(smithfox注: GraphicElement是spark的类, 确实是少有继承层次非常少的对象, 如图:)
IVisualElementContainer定义了contentAPIs. 在Spark中,Skin,Group, 和SkinnableContainer实现了这个接口,持有着可视化元素. 为保持一致性, MX的Container也实现了这个接口, 不过只是对addChild(), numChildren, 等函数的封装....
这个接口使访问树变得容易了. 本质上, 这个接口为容器对外暴露有它哪些孩子提供了方法. 例如,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中,IVisualElementContainer接口可以让你访问孩子. 这也是Spark组件宣布谁是他的可视化孩子的方式.Group和SkinnableContainer都实现了这个接口. 另外, MX的Container也实现了这个接口. 但那只是对显示列表APIs的一种封装,IVisualElementContainer提供了唯一的,一致的访问容器孩子的方法.
在Spark中,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用来容纳数据项.Group和SkinnableContainer用来容纳可视化元素. 一个数据容器能容纳任何东西, 但特别是用来容纳非可视元素 (比如.-真正的数值). 有关数据容器重要的一点是它们支持 项渲染,就是将数据项转换为可视元素.
项渲染
DataGroup有能力将随意的非可视化元素呈现到屏幕. 因此, 项渲染器正好可以加到布局树中. 某些情况下, 甚至于可视化元素,比如UIComponents和GraphicElements, 也被包装成项渲染器. 为向开发人员展现这个设计思路,我们考虑一下以下几个可选方案:
- DataGroup 和 SkinnableDataContainer 设计成叶子节点, 他们的实际可视化孩子不能被访问
- DataGroup 和 SkinnableDataContainer 实现IVisualElementContainer接口. 当问屏幕上有几个可视化元素时, 我们只返回当前屏幕正在被渲染的那些元素. Mutation APIs RTE(RuntimeException).
- DataGroup 和 SkinnableDataContainer 实现IVisualElementContainer接口. 当问屏幕上有几个可视化元素时, 我们返回所有项个数. 如果用户访问一个还未曾被渲染过的项时, 我们就创建并且渲染.
我们决定向DataGroup增加"只读"的 element APIs, 象numElements,getElementAt(), 和getElementIndex(). 还有另一个API,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, 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 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, 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;
[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, 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;
}
[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. 但现在还是,parent这个名字比其它名字要适合一些. 如果我们决定使容器变成非DisplayObjects, 那时我们可能会贯彻到底, 将所有的DisplayObject都变成是可选的l.
- Walking the layout tree requires knowledge ofSkinnableComponentandSkin. 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, numChildren, etc...)调用时也抛出异常, 但会有架构方面的问题, 比如UIComponent的 removeChildAt 方法就依赖于这些API.如果这原来是一个优先事项, 我们可以在这些方法中都加入运行异常并且提供新的方法, 比如 $getChildAt_SkinnableComponent之类. 然后我们在这些新API的基础上改动所有framework的代码. 这样做又有新的问题:[Child APIs vs. Item APIs].