增量UI投影:避免整树重建后,别陷入平行Map地狱

自动关联目录:增量UI投影:避免整树重建后,别陷入平行Map地狱

核心判断:用数据对象树驱动 UWidget/Slate 增量投影,比每次变化重建整棵 UI 正确;但若每个属性、控件和暂态都拥有一张独立 Map,创建、更新、销毁和诊断会形成无法证明完整性的平行状态。

一、已有设计中值得保留的部分

运行时把创作者 UI 保存为数据节点,原生层监听节点增加、属性变化、父级变化和销毁,再增量创建或更新 UWidget。图片依赖和祖先布局变化会先标脏,在维护 Tick 合并处理。它把数据树设为事实、Widget 设为可丢弃投影,也避免一处小改动重建整树。

真正的复杂度转移到了管理器内部:节点与 Widget、布局、样式、图片、输入、状态等分别进入多张映射;销毁时逐张清理。新增一种属性往往要同时修改创建、变更、祖先失效、资源完成、销毁和诊断路径,漏一处就产生残留或旧值。

二、问题本质:投影没有自己的聚合边界

平行 Map 让一个节点的状态分散在多个容器,类型系统无法回答:这个节点是否完整创建、哪些字段已应用、哪个异步资源仍属于当前 Widget、销毁是否清干净。全局管理器也逐渐同时承担文档索引、Widget 工厂、布局、资源、输入和生命周期。

三、目标设计:每个节点一个投影状态,各系统消费 Patch

struct NodeProjection {
    NodeId id;
    Generation generation;
    TWeakObjectPtr<UWidget> widget;
    AppliedRevision revision;
    DirtyBits dirty;
    LayoutState layout;
    StyleState style;
    ResourceTickets resources;
    BindingHandles bindings;
};

Patch Diff(NodeSnapshot oldState, NodeSnapshot newState);
void ApplyPatch(NodeProjection& p, Patch patch, FrameBudget& budget);
void Destroy(NodeProjection&& p); // 集中取消资源、解绑、释放 Widget

布局、样式、图片和输入系统只消费自己负责的 Patch;节点投影持有完整资源票据和绑定句柄。Widget 类型变化时提升 generation,旧图片回调和旧 Slate 事件自然失效。

四、批处理不是无限延迟

属性变化先合并 DirtyBits,但命中测试前必须满足布局 Revision 屏障;否则画面仍在旧位置、输入却按新数据判断。可见节点和交互节点优先,离屏样式更新可延后。预算应记录待处理节点年龄,不能只限制每帧数量。

五、验证与反思

随机生成节点增加、重父、类型替换、图片晚到和销毁序列,对比“从数据树全量重建”的参考结果;每帧检查节点投影数、存活 Widget 数和绑定句柄守恒。性能指标同时看 Patch 数、合并率、队列年龄和最坏单节点成本。

经验不是拒绝集中管理器,而是让它成为调度器:节点状态有聚合边界,领域系统有明确所有权,数据树仍是唯一事实。