从Flex中owner和parent的区别来说Flex API设计思路
这篇文章是拷贝过来的,读完确实让我对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可以访问他们. 这些content APIs很像flash DisplayObjectContainer 的 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) or myPanel.contentGroup.getElementAt(0) or myPanel.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 定义了content APIs. 在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 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, numChildren, etc...)调用时也抛出异常, 但会有架构方面的问题, 比如UIComponent的 removeChildAt 方法就依赖于这些API. 如果这原来是一个优先事项, 我们可以在这些方法中都加入运行异常并且提供新的方法, 比如 $getChildAt_SkinnableComponent之类. 然后我们在这些新API的基础上改动所有framework的代码. 这样做又有新的问题: [Child APIs vs. Item APIs].