kunpolibrary/docs/Data.md
gongxh e48011d941 UI模块添加数据绑定装饰器
1.添加数据基类,子类自动添加代理,数据变化自动通知
 2.支持同属性多装饰器
2025-09-01 09:46:16 +08:00

7.4 KiB
Raw Permalink Blame History

数据绑定Data Binding

本库的数据绑定面向“简单直接”的 UI 同步:当数据类的属性被设置或数据类上的公开方法被调用时,触发与之匹配的装饰器回调,用最少样板代码完成 UI 更新。复杂交互(动画、跨模块协作、节流/并发控制)仍建议使用事件系统或显式逻辑处理。

  • 优势
    • 精简:无需注册/注销一堆事件监听,几行装饰器即可完成 UI 同步
    • 类型安全:通过 selector 函数选择数据路径,配合 TS 编译期检查
    • 零侵入:数据类只需继承 DataBase,代理自动拦截属性设置与方法调用
    • 高性能:同一帧内变更合并批量分发(BatchUpdater
  • 适用边界
    • 简单、直接的属性展示或方法触发的轻量级反馈
    • 复杂动画链路、跨系统通信、长时流程控制 → 使用事件系统更明确可控

基本概念

  • 数据类:继承 DataBase。属性赋值和公开方法调用都会发出变更通知
  • 绑定路径:ClassName:memberName,由装饰器通过 selector 推导
  • selector 约束:仅支持 d => d.xxx.yyyfunction(d){ return d.xxx.yyy; } 的直链式访问;不支持动态表达式/可选链/解构
  • 触发时机:
    • 属性变更:设置新值且与旧值不同才触发(以 _ 开头的属性被忽略)
    • 方法调用:公开方法被调用即触发(constructor 与以 _ 开头的方法被忽略)
  • 生命周期:
    • 初始化:data.initializeBindings(this)FGUI/窗口基类已内置自动调用)
    • 清理:data.cleanupBindings(this)FGUI 组件 dispose 已内置调用)
  • 即时更新:装饰器 immediate 参数,默认 false为 true 时属性变更会立刻执行一次回调

API 速览

import { data } from "kunpocc"; // 实际从 src/data/DataDecorator 导出
import { DataBase } from "kunpocc";

// 属性绑定装饰到“UI字段”上
data.bindProp<T extends DataBase>(
  dataClass: new () => T,
  selector: (d: T) => any,
  callback: (item: any, value?: any, data?: T) => void,
  immediate: boolean = false,
)

// 方法绑定装饰到“UI方法”上
data.bindMethod<T extends DataBase>(
  dataClass: new () => T,
  selector: (d: T) => any, // 选择数据类上的某个“公开方法”
  immediate: boolean = false,
)

回调参数说明(属性绑定):

  • item:被装饰的 UI 字段(例如某 Label
  • value:属性新值(仅属性变更时有值)
  • data:当前数据类实例(便于读取其它字段)

方法绑定回调时机:当目标数据方法被调用后触发,默认不传 value,你可以在回调内读取 data 的当前状态。


使用方式(从简到难)

1) 属性装饰器(同一窗口类中合并展示“单个/多个装饰器”)

import * as fgui from "fairygui-cc";
import { DataBase, data, Window, _uidecorator } from "kunpocc";

const { uiclass, uiprop } = _uidecorator;

// 示例数据类
class Level extends DataBase {
  public levelid = 1;
  public storey = 1;
}

// 使用窗口(继承 kunpo.Window同一类中分别演示
// A) 单个装饰器    B) 多个装饰器(同一 UI 属性上堆叠多个装饰器)
@uiclass("Window", "Data", "DataWindow")
export class DataWindow extends Window {
  // --- A) 单个装饰器:仅响应 storey ---
  @uiprop
  @data.bindProp(Level, d => d.storey, (item: fgui.GTextField, value: number) => {
    item.text = `层数:${value}`;
  })
  private lab_storey!: fgui.GTextField;

  // --- B) 多个装饰器:同一属性上堆叠,分别响应 storey 与 levelid ---
  @uiprop
  @data.bindProp(Level, d => d.storey, (item: fgui.GTextField, value: number, model: Level) => {
    item.text = `关卡:${model.levelid} 层数:${value}`;
  })
  @data.bindProp(Level, d => d.levelid, (item: fgui.GTextField, value: number, model: Level) => {
    item.text = `关卡:${value} 层数:${model.storey}`;
  })
  private lab_level!: fgui.GTextField;
}

// 触发(示意)
const level = new Level();
level.storey = 2;  // 触发 lab_storey 与 lab_level 的第一个装饰器
level.levelid = 10; // 触发 lab_level 的第二个装饰器

要点:

  • A/B 两段示例均在“窗口类”中,满足“属性装饰器使用窗口”的要求
  • “多个装饰器”强调“同一个 UI 属性”上堆叠多个 @bindProp,参考 demo 的 DataWindow.tsDataItem.ts
  • immediate 可按需加在装饰器末参数,设为 true 时“属性变更会立即执行一次回调”(首帧对齐)

2) 方法装饰器(同一自定义组件中合并展示“单个/多个装饰器”)

import * as fgui from "fairygui-cc";
import { DataBase, data, _uidecorator } from "kunpocc";

const { uicom } = _uidecorator;

// 示例数据类
class Inventory extends DataBase {
  public addItem(it: string) { /* ... */ }
  public reset() { /* ... */ }
}

// 使用自定义组件(继承 fgui.GComponent同一类中分别演示
// A) 单个方法装饰器    B) 多个方法装饰器(同一 UI 方法上堆叠多个装饰器)
@uicom("Data", "DataItem")
export class DataItem extends fgui.GComponent {
  // --- A) 单个方法装饰器:仅响应 addItem ---
  @data.bindMethod(Inventory, d => d.addItem)
  onAddItem(inv: Inventory) {
    // 轻量反馈:刷新一次列表/播放提示
    // this.refresh(inv);
  }

  // --- B) 多个方法装饰器:同一 UI 方法上堆叠,响应 reset 与 addItem ---
  @data.bindMethod(Inventory, d => d.reset)
  @data.bindMethod(Inventory, d => d.addItem)
  onAny(inv: Inventory) {
    // 统一刷新
    // this.refresh(inv);
  }
}

// 触发(示意)
const inv = new Inventory();
inv.addItem("Potion"); // 触发 onAddItem 与 onAny
inv.reset();            // 仅触发 onAny

要点:

  • 示例放在“自定义组件”中,满足“方法装饰器使用自定义组件”的要求
  • “多个装饰器”强调“同一个 UI 方法”上堆叠多个 @bindMethod,参考 demo 的 DataWindow.tsrefreshData
  • 方法绑定不关心入参与返回值,调用即触发;复杂动画仍建议事件系统编排

生命周期与集成

  • FGUI 自定义组件与窗口基类已内置:
    • 构造onConstructdata.initializeBindings(this)
    • dispose() 时:data.cleanupBindings(this)
  • 非集成场景(如普通类):
    • 初始化后手动调用 data.initializeBindings(this)
    • 销毁前调用 data.cleanupBindings(this)

参考代码位置:

  • src/ui/ComponentExtendHelper.ts 内在构造与 dispose 中已自动处理
  • src/fgui/WindowBase.ts 同样在生命周期中调用了初始化

最佳实践与注意事项

  • 关注点内聚:属性绑定仅做“值到视图”的即时映射;复杂动作用事件系统
  • 避免回调里递归触发同一数据的写操作,防止无意义的级联更新
  • ___ 开头的属性/方法不会触发绑定,不要用作绑定目标
  • selector 必须是直链式访问:d => d.foo.bar,不要写表达式/调用/条件语句
  • immediate 用于是否立即触发更新,默认关闭以避免不必要开销

调试小贴士

import { BindManager } from "kunpocc";
BindManager.getAllPaths();         // 查看已注册的所有路径
BindManager.getTotalBindingCount(); // 总绑定数量

如果绑定无效:检查数据类是否继承了 DataBaseselector 是否为直链式、是否在生命周期内完成了 initializeBindings