系列 · 为 UITK 写一套模板系统 第 3 / 7 篇

继承链:模板组件由哪四层组成

四层,从引擎到模板

如果只看模板装配的基础继承链,这套系统里的一个模板组件至少落在下面四层之上:

VisualElement                     ← UITK 原生元素:进树、布局、被样式选中
└─ ContextElement                 ← 提供 / 注入上下文
   └─ RuntimeElement              ← 挂载生命周期、完成链、响应式 Effect
      └─ TemplateElement          ← 构造期装配:模板 + 样式 + 元数据(本系列主线)
层类(真实定义)加的能力
1VisualElement(引擎)进树、布局、样式、事件——UITK 一切 UI 的根
2ContextElement : VisualElement向子树提供上下文、从祖先解析上下文
3RuntimeElement : ContextElement挂载生命周期(完成链)、响应式 Effect 与清理
4TemplateElement : RuntimeElement构造期装配:按类型加载 UXML / USS 与元数据

越往下越接近模板——模板能力只出现在最底层的 TemplateElement 上;越往上越通用,回到引擎本身。实际业务组件当然还可以在这四层之上继续叠加更多语义层,但这四层是后文讨论模板机制时默认共享的基础地基。本篇逐层拆开,看每一层各自回答什么问题、为什么值得单独占一层。

第 1 层:VisualElement——先是一个普通元素

引擎的 VisualElement 是 UITK 里所有 UI 的根。能 Add 子树、能布局、能被 USS 规则选中、能收发事件——一个模板组件出生时就拥有这一切,因为它首先就是 VisualElement。

这一层不需要任何代码。它划定的是心理边界:模板组件没有特权,参与的是和其他元素完全相同的树与布局系统。后面的文章会反复看到这种「先是普通元素,再叠加能力」的写法——它是这套系统不推翻引擎的起点。

第 2 层:ContextElement——上下文注入

问题:一棵深树里,某个祖先组件持有它的深层后代需要的东西——一组配置、一份数据、一种服务。把它们送下去的最直白写法是逐层构造传参,代价是每一层中间节点都要感知与自身无关的依赖——这是样板,也是耦合。

// ContextElement.cs(节选)
public class ContextElement : VisualElement
{
    public void Provide<T>(T context) where T : class { ... }

    public T Inject<T>() where T : class
    {
        return InjectFrom<T>(parent);
    }
}

Provide<T> 把实例登记为自己向子树提供的上下文;Inject<T> 沿 parent 链向上找最近的提供者。组件在自己的子树里随便注入,拿到的总是离自己最近的那份——这对应 Web 里的 provide / inject:显式提供,就近解析,不经过中间层。

两个实现细节值得注意:

ContextElement 的消费者不止模板组件:任何需要向子树提供数据、配置或服务的界面容器,都可以只继承到这一层——注入能力不依赖模板、也不依赖运行时,它是四层里第一个可独立复用的通用层。

第 3 层:RuntimeElement——挂载生命周期与响应式

模板组件的多数行为发生在「挂到面板之后」:等整棵模板树稳定、建立绑定、响应数据变化。UITK 原生只有 AttachToPanelEvent 一个信号,先后顺序、树是否稳定、重复触发都要自己处理——这套系统需要自己的生命周期语义。

// RuntimeElement.cs(节选)
public class RuntimeElement : ContextElement
{
    public RuntimeElement()
    {
        RegisterCallback<AttachToPanelEvent>(OnAttachedToPanel);
        RegisterCallback<DetachFromPanelEvent>(OnDetachedFromPanel);
    }
}

RuntimeElement 在构造时接管 attach / detach,对外给出三个公开时机:OnAttaching(attach 早期,自身已挂上,但祖先编排可能未完、后代也未必完成,树不可假设稳定)、OnMounted(整棵子树完成,树稳定,是真正的“就绪”信号)、OnUnmounted(从 panel 卸下,释放这一轮挂载建立的资源)。其中 OnMounted 由一条完成链驱动:父先子后地 attach,子先父后地报告完成,计数配平后在父级收口——防重、复位、快照过期重统计都在这一层。这是后续实现篇会反复回来的主角机制,这里只标记它的位置:它不依赖模板——完成链服务的是任何需要在稳定树上动手的组件。

同一层还有响应式的一半:CreateEffect 建立读 Ref 即订阅的副作用,disposer 进 _disposables,随元素生命周期统一清理。

响应式的完整语义——Ref 订阅、Computed 推导、Effect 清理、生命周期——是响应系统开发文章的主题:那是与本系列并列的一支实现记录,Ref 怎么做到读即订阅、Effect 何时建立何时清理,都会在那一支的文章里逐层展开。本系列不展开它,只消费它一个接口面:模板的每个绑定最终都是一个 Effect——后续实现篇会看到它在哪里创建、何时释放。

第 4 层:TemplateElement——模板装配

模板机制从这一层才开始。严格说,引擎的 VisualElement 本身就消费 UXML 与 USS——任何元素都能被 UXML 实例化、都能挂 styleSheets;前三层缺的不是「认识」,而是模板装配:按类型找模板资产、克隆成树、挂进样式链、取出元数据——这些代码在前三层一行都没有。模板概念从 TemplateElement 开始:

// TemplateElement.cs(节选)
public class TemplateElement : RuntimeElement
{
    public TemplateElement()
    {
        LoadAssets();
    }
}

当最外层模板容器被实例化时,这一层会按真实类型查生成期索引,把查到的 UXML 克隆成树、USS 挂进样式链、元数据取出备好。手动触发点通常只在根组件这一处;模板里声明的子组件,会在后续 CloneTree 过程中由系统内部继续递归装配。它不渲染自己,也没有外观——职责只有一件:把「类 → 模板资源 → UI」这条链变成构造期的事实。这一层的完整拆解(加载链、缓存、路径解析)是下一篇的主题,这里不展开。

为什么是四层

分层的主线是与模板的耦合度:越往上越通用,越往下越模板化。树上的前三层完全不碰模板概念——ContextElement 可以被任何需要注入的界面复用,RuntimeElement 的生命周期语义服务于一切「要等树稳定」的组件;直到最底一层的 TemplateElement 才加上模板装配——把「类」与它的 UXML / USS / 元数据接起来的那条链。模板能力不是继承链自带的,而是沿着这条基础链走到底之后,由这一层加上去的。

模板组件与普通元素的基础分界,就是有没有落到这条链的最底层:落到 TemplateElement,就拥有这四层基础能力——继承是入场券。更深的层(指令编译、绑定、组件库)会在这四层之上继续叠加,并随本系列逐篇展开;到 TemplateElement 为止,是本文的地图边界。

下一篇

组件配方:cs / uxml / uss 三块资产如何拼成一个可自动装配的组件——把第 4 层的 LoadAssets 拆开看。

本文在 AI 协作下完成。