UI模块添加数据绑定装饰器

1.添加数据基类,子类自动添加代理,数据变化自动通知
 2.支持同属性多装饰器
This commit is contained in:
gongxh
2025-08-29 15:25:10 +08:00
parent b62a4af8db
commit e48011d941
45 changed files with 1354 additions and 21 deletions

125
src/data/BatchUpdater.ts Normal file
View File

@@ -0,0 +1,125 @@
import { BindManager } from './BindManager';
import { BindInfo, IDataEvent } from './types';
/**
* 挂起更新信息
*/
interface PendingUpdate {
/** 绑定器信息 */
info: BindInfo;
/** 路径变化事件 */
event: IDataEvent;
}
/**
* 批量更新调度器
* 负责将同一帧内的多次数据变化合并为一次更新,提升性能
*/
export class BatchUpdater {
/** 挂起的更新任务集合 */
private static pendingUpdates = new Map<string, PendingUpdate>();
/** 是否已调度批量更新 */
private static isScheduled = false;
/** 立即更新的绑定器集合(防重复触发) */
private static immediateUpdates = new Set<string>();
/**
* 通知所有匹配的绑定器
* @param event 路径变化事件
*/
public static notifyBindings(event: IDataEvent): void {
const bindInfos = BindManager.getMatchingBindings(event.path);
for (const info of bindInfos) {
if (info.immediate) {
// 立即更新模式
this.executeImmediateUpdate(info, event);
} else {
// 批量更新模式
this.scheduleBatchUpdate(info, event);
}
}
}
/**
* 执行立即更新(防止同一帧内重复触发)
* @param info 绑定器
* @param event 变化事件
*/
private static executeImmediateUpdate(info: BindInfo, event: IDataEvent): void {
const key = this.getBindingKey(info);
// 防止同一帧内重复执行
if (this.immediateUpdates.has(key)) {
return;
}
this.immediateUpdates.add(key);
try {
info.callback.call(info.target, event);
} catch (error) {
console.error(`绑定器回调执行失败,路径:${event.path}`, error);
} finally {
// 下一帧清理标记
setTimeout(() => {
this.immediateUpdates.delete(key);
}, 0);
}
}
/**
* 调度批量更新
* @param info 绑定器
* @param event 变化事件
*/
private static scheduleBatchUpdate(info: BindInfo, event: IDataEvent): void {
const key = this.getBindingKey(info);
// 同一绑定器在一帧内只保留最后一次更新
this.pendingUpdates.set(key, { info, event });
// 如果还未调度,则调度一次批量更新
if (!this.isScheduled) {
this.isScheduled = true;
setTimeout(() => this.flush(), 0);
}
}
/**
* 执行所有挂起的更新任务
*/
private static flush(): void {
// 先复制当前状态
// 清理原始状态
// 安全处理复制的数据
const updates = Array.from(this.pendingUpdates.values());
this.pendingUpdates.clear();
this.isScheduled = false;
for (const { info, event } of updates) {
try {
let target = info.target;
if (info.isMethod) {
info.callback.call(target, event.target);
} else {
info.callback.call(target, target[info.prop], event.isProp ? event.value : undefined, event.target);
}
} catch (error) {
// 单个绑定器异常不影响其他绑定器的执行
console.error(`绑定器回调执行失败,路径:${event.path}`, error);
}
}
}
/**
* 生成绑定器唯一键
* @param binding 绑定器
*/
private static getBindingKey(info: BindInfo): string {
if (info.isMethod) {
return `${info.target.__data_id__}:${info.prop.toString()}`;
}
return `${info.target.__data_id__}:${info.prop.toString()}:${info.path}`;
}
}

90
src/data/BindManager.ts Normal file
View File

@@ -0,0 +1,90 @@
import { BindInfo } from './types';
export class BindManager {
/**
* 绑定器集合
* 键:路径
* 值:绑定器集合
*/
private static _bindings = new Map<string, Set<BindInfo>>();
static addBinding(info: BindInfo): void {
// 延迟初始化:在第一次添加绑定时确保实例已正确初始化
this._ensureInstanceInitialized(info.target);
if (!this._bindings.has(info.path)) {
this._bindings.set(info.path, new Set());
}
this._bindings.get(info.path)!.add(info);
}
/**
* 确保实例已正确初始化(延迟初始化策略)
* 这样可以适配所有场景:独立使用、@uicom、@uiclass
*/
private static _ensureInstanceInitialized(instance: any): void {
// 如果已经初始化过,直接返回
if (instance.__bindings_initialized__) {
return;
}
const ctor = instance.constructor as any;
// 生成唯一ID
if (!instance.__data_id__) {
instance.__data_id__ = `${ctor.name}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
// 标记已初始化
instance.__bindings_initialized__ = true;
}
static removeBinding(info: BindInfo): void {
const pathBindings = this._bindings.get(info.path);
if (pathBindings) {
pathBindings.delete(info);
if (pathBindings.size === 0) {
this._bindings.delete(info.path);
}
}
}
static getMatchingBindings(path: string): Set<BindInfo> {
// 直接通过路径获取绑定器集合,避免不必要的遍历
return this._bindings.get(path) || new Set<BindInfo>();
}
static cleanup(target: any): void {
const toRemove: BindInfo[] = [];
for (const bindingSet of this._bindings.values()) {
bindingSet.forEach(binding => {
if (binding.target === target) {
toRemove.push(binding);
}
});
}
toRemove.forEach(binding => this.removeBinding(binding));
}
static clearAll(): void {
this._bindings.clear();
}
/************** 调试用 **************/
static getBindingsForPath(path: string): Set<BindInfo> {
return this._bindings.get(path) || new Set();
}
static getTotalBindingCount(): number {
let count = 0;
for (const bindingSet of this._bindings.values()) {
count += bindingSet.size;
}
return count;
}
static getAllPaths(): string[] {
return Array.from(this._bindings.keys());
}
/************** 调试用 **************/
}

45
src/data/DataBase.ts Normal file
View File

@@ -0,0 +1,45 @@
import { BindManager } from './BindManager';
import { ProxyObject } from './ProxyHandler';
import { BindInfo } from './types';
/**
* 响应式数据基类
* 通过 Proxy 拦截属性访问,实现零侵入式响应式数据绑定
*/
export class DataBase {
/** 响应式对象唯一标识 */
private __data_id__: string;
/** 绑定器集合 */
private __watchers__: Set<BindInfo>;
/** 是否已销毁 */
private __destroyed__: boolean = false;
constructor() {
// 返回包装后的对象,自动使用 constructor.name
return ProxyObject(this);
}
/**
* 销毁响应式对象,清理所有绑定器
*/
public destroy(): void {
this.__destroyed__ = true;
this.__watchers__.clear();
BindManager.cleanup(this);
}
/**
* 获取响应式对象ID
*/
public getDataId(): string {
return this.__data_id__;
}
/**
* 检查是否已销毁
*/
public isDestroyed(): boolean {
return this.__destroyed__;
}
}

125
src/data/DataDecorator.ts Normal file
View File

@@ -0,0 +1,125 @@
import { BindManager } from './BindManager';
import { DataBase } from './DataBase';
import { BindInfo } from './types';
export namespace data {
/**
* @bindAPI 装饰器元数据存储键
*/
const BIND_METADATA_KEY = Symbol('__bind_metadata__');
/**
* 为实例初始化绑定器(在装饰器收集完所有绑定信息后调用)
* @param instance 目标实例
*/
export function initializeBindings(instance: any) {
const ctor = instance.constructor as any;
const binds = ctor[BIND_METADATA_KEY] || [];
for (const info of binds) {
const bindInfo: BindInfo = {
target: instance,
prop: info.prop,
callback: info.isMethod ? instance[info.prop].bind(instance) : info.callback.bind(instance),
path: info.path,
immediate: info.immediate,
isMethod: info.isMethod
};
// 注册到全局绑定器管理器BindManager 会自动处理延迟初始化)
BindManager.addBinding(bindInfo);
}
}
/**
* 强类型属性绑定装饰器
*
* @param dataClass 显示传入数据类,用于获取类名
* @param selector 类型安全的路径选择器函数
* @param callback.item: 当前装饰的类属性
* @param callback.value: 如果绑定的是数据属性则value为数据属性值否则为undefined
* @param callback.data: 数据类实例
* @param immediate 是否立即触发默认true
*/
export function bindProp<T extends DataBase>(dataClass: new () => T, selector: (data: T) => any, callback: (item: any, value?: any, data?: T) => void, immediate: boolean = false) {
return function (target: any, prop: string | symbol) {
// 解析路径
const path = `${dataClass.name}:${extractPathFromSelector(selector)}`;
// console.log('绑定属性的监听路径', path);
let ctor = target.constructor;
// 存储绑定元数据
ctor[BIND_METADATA_KEY] = ctor[BIND_METADATA_KEY] || [];
ctor[BIND_METADATA_KEY].push({
target: null,
prop,
callback,
path: path,
immediate,
isMethod: false
});
};
}
/**
* 强类型方法绑定装饰器
* 在编译期验证路径有效性,防止重构时出现绑定失效
*
* @param dataClass 显示传入数据类,用于获取类名
* @param selector 类型安全的路径选择器函数
* @param immediate 是否立即触发默认false
*/
export function bindMethod<T extends DataBase>(dataClass: new () => T, selector: (data: T) => any, immediate: boolean = false) {
return function (target: any, method: string | symbol, descriptor?: PropertyDescriptor) {
// 解析路径
const path = `${dataClass.name}:${extractPathFromSelector(selector)}`;
// console.log('绑定方法的监听路径', path);
// 存储绑定元数据
let ctor = target.constructor;
ctor[BIND_METADATA_KEY] = ctor[BIND_METADATA_KEY] || [];
ctor[BIND_METADATA_KEY].push({
target: null,
prop: method,
callback: descriptor!.value,
path: path,
immediate,
isMethod: true
});
return descriptor;
};
}
/**
* 从选择器函数中提取路径字符串
* 这是运行时路径解析配合TypeScript编译期检查使用
*/
function extractPathFromSelector(selector: Function): string {
const fnString = selector.toString();
// 匹配箭头函数: data => data.property.path
let match = fnString.match(/\w+\s*=>\s*\w+\.(.+)/);
if (!match) {
// 匹配普通函数: function(data) { return data.property.path; }
match = fnString.match(/return\s+\w+\.(.+);?\s*}/);
}
if (!match) {
throw new Error('无效的路径选择器函数,请使用 data => data.property.path 或 function(data) { return data.property.path; } 的形式');
}
return match[1].trim();
}
/**
* 手动清理目标对象的所有绑定器
* @param target 目标对象
*/
export function cleanupBindings(target: any): void {
BindManager.cleanup(target);
if (target.__watchers__) {
target.__watchers__.clear();
}
}
}

167
src/data/ProxyHandler.ts Normal file
View File

@@ -0,0 +1,167 @@
/**
* @Author: Gongxh
* @Date: 2025-08-26
* @Description:
*/
import { BatchUpdater } from "./BatchUpdater";
import { IDataEvent } from "./types";
// 全局唯一ID生成器
let nextId = 1;
function notifyChange(path: string, dataInstance: any, value?: any, isProp: boolean = false) {
const event: IDataEvent = {
path,
target: dataInstance,
value,
isProp
};
// console.log('发出的通知路径', path);
BatchUpdater.notifyBindings(event);
}
/**
* 处理属性设置的拦截逻辑
*/
function handlePropertySet(dataInstance: any, target: any, prop: string | symbol, value: any): boolean {
let oldValue = Reflect.get(target, prop);
if (oldValue === value) {
// 数据不变 无需通知 无需修改
return true;
}
let propname = prop.toString();
// 排除以_和__开头的方法
if (propname.startsWith('_')) {
Reflect.set(target, prop, value);
return true;
}
const result = Reflect.set(target, prop, value);
if (!dataInstance.__destroyed__) {
const path = `${dataInstance.constructor.name}:${propname}`;
notifyChange(path, dataInstance, value, true);
}
return result;
}
/**
* 处理方法拦截的逻辑只拦截数据类中的方法排除constructor
*/
function handleMethodGet(dataInstance: any, target: any, prop: string | symbol, receiver: any): any {
const value = Reflect.get(target, prop, receiver);
const propname = prop.toString();
// 如果不是函数,直接返回
if (typeof value !== 'function') {
return value;
}
// 排除constructor方法
if (propname === 'constructor') {
return value;
}
// 排除以_和__开头的方法
if (propname.startsWith('_')) {
return value;
}
// 如果已经包装过,直接返回
if (value.__kunpo_wrapped__) {
return value;
}
const wrappedFunc = new Proxy(value, {
apply: function (target: any, thisArg: any, args: any[]): any {
// console.log('拦截到函数调用:', propname, args);
let result = Reflect.apply(target, thisArg, args);
const path = `${dataInstance.constructor.name}:${propname}`;
notifyChange(path, dataInstance);
return result;
}
});
// 标记已包装,避免重复包装
Object.defineProperty(wrappedFunc, '__kunpo_wrapped__', {
value: true,
writable: false,
enumerable: false,
configurable: false
});
// 缓存包装后的函数
Reflect.set(target, prop, wrappedFunc);
return wrappedFunc;
}
/**
* 设置对象的内部属性
*/
function setupInternalProperties(dataInstance: any): void {
// 使用构造函数名作为类名,与装饰器保持一致
const className = dataInstance.constructor.name;
dataInstance.__data_id__ = `${className}-${nextId++}`;
dataInstance.__watchers__ = new Set();
// 定义不可枚举的内部属性,防止代码混淆问题
Object.defineProperty(dataInstance, '__data_id__', {
value: dataInstance.__data_id__,
writable: false,
enumerable: false,
configurable: false
});
Object.defineProperty(dataInstance, '__watchers__', {
value: dataInstance.__watchers__,
writable: false,
enumerable: false,
configurable: false
});
Object.defineProperty(dataInstance, '__destroyed__', {
value: false,
writable: true,
enumerable: false,
configurable: false
});
}
/**
* 初始化已存在的直接子属性不包含以_和__开头的属性不进行深层递归
*/
function initializeDirectProperties(dataInstance: any): void {
for (const key in dataInstance) {
// 跳过以_和__开头的属性和函数
if (key.startsWith('_') || typeof dataInstance[key] === 'function') {
continue;
}
const value = dataInstance[key];
if (typeof value === 'object' && value !== null && !(value as any).__data_id__) {
// 简单标记为已包装,但不创建深层代理
Object.defineProperty(value, '__data_id__', {
value: `${dataInstance.__data_id__}:${key}`,
writable: false,
enumerable: false,
configurable: false
});
}
}
}
export function ProxyObject(dataInstance: any) {
const handler = {
set: (target: any, prop: string | symbol, value: any): boolean => {
return handlePropertySet(dataInstance, target, prop, value)
},
get: (target: any, prop: string | symbol, receiver: any): any => {
return handleMethodGet(dataInstance, target, prop, receiver)
}
};
setupInternalProperties(dataInstance);
initializeDirectProperties(dataInstance);
return new Proxy(dataInstance, handler);
}

31
src/data/types.ts Normal file
View File

@@ -0,0 +1,31 @@
/**
* 绑定器信息
*/
export interface BindInfo {
/** 监听目标对象 */
target: any;
/** 属性或方法名 */
prop: string | symbol;
/** 监听的路径 */
path: string;
/** 回调函数 */
callback: Function;
/** 是否立即更新 */
immediate: boolean;
/** 是否为方法监听 */
isMethod: boolean;
}
/**
* 路径变化事件
*/
export interface IDataEvent {
/** 变化的属性路径 */
path: string;
/** 目标对象 */
target: any;
/** 是否是属性变化 */
isProp?: boolean;
/** 变化后的值 */
value?: any;
}

View File

@@ -5,6 +5,7 @@
*/
import { GComponent } from "fairygui-cc";
import { data } from "../data/DataDecorator";
import { Screen } from "../global/Screen";
import { AdapterType, WindowType } from "../ui/header";
import { IWindow } from "../ui/IWindow";
@@ -49,6 +50,8 @@ export abstract class WindowBase extends GComponent implements IWindow {
// 窗口自身也要设置是否吞噬触摸
this.opaque = swallowTouch;
this.bgAlpha = bgAlpha;
// 初始化数据绑定(如果有 @dataclass 装饰器)
data.initializeBindings(this);
this.onInit();
}
@@ -79,6 +82,8 @@ export abstract class WindowBase extends GComponent implements IWindow {
* @internal
*/
public _close(): void {
// 窗口关闭时 清理绑定信息
data.cleanupBindings(this);
this.onClose();
this.dispose();
}

View File

@@ -44,3 +44,7 @@ export { BytedanceCommon } from "./minigame/bytedance/BytedanceCommon";
export { MiniHelper } from "./minigame/MiniHelper";
export { WechatCommon } from "./minigame/wechat/WechatCommon";
/** 数据绑定相关 - 强类型数据绑定系统 */
export { DataBase } from './data/DataBase';
export { data } from './data/DataDecorator';

View File

@@ -4,6 +4,7 @@
* @Description: 自定义组件扩展帮助类
*/
import { UIObjectFactory } from "fairygui-cc";
import { data } from "../data/DataDecorator";
import { debug } from "../tool/log";
import { PropsHelper } from "./PropsHelper";
import { _uidecorator } from "./UIDecorator";
@@ -51,9 +52,19 @@ export class ComponentExtendHelper {
// 自定义组件扩展
const onConstruct = function (this: any): void {
PropsHelper.serializeProps(this, pkg, name);
// 初始化数据绑定(如果有 @dataclass 装饰器)
data.initializeBindings(this);
this.onInit && this.onInit();
};
ctor.prototype.onConstruct = onConstruct;
const dispose = ctor.prototype.dispose
const newDispose = function (this: any): void {
data.cleanupBindings(this);
dispose.call(this);
};
ctor.prototype.dispose = newDispose;
// 自定义组件扩展
UIObjectFactory.setExtension(`ui://${pkg}/${name}`, ctor);
}

View File

@@ -62,11 +62,11 @@ export namespace _uidecorator {
*/
export function uiclass(groupName: string, pkgName: string, name: string, bundle?: string): Function {
/** target 类的构造函数 */
return function (ctor: any): void {
// debug(`uiclass >${JSON.stringify(res)}<`);
// debug(`uiclass prop >${JSON.stringify(ctor[UIPropMeta] || {})}<`);
uiclassMap.set(ctor, {
ctor: ctor,
return function (ctor: any): any {
// 检查是否有原始构造函数引用(由其他装饰器如 @dataclass 提供)
const originalCtor = ctor;
uiclassMap.set(originalCtor, {
ctor: ctor, // 存储实际的构造函数(可能被包装过)
props: ctor[UIPropMeta] || null,
callbacks: ctor[UICBMeta] || null,
controls: ctor[UIControlMeta] || null,
@@ -78,8 +78,9 @@ export namespace _uidecorator {
bundle: bundle || "",
},
});
// 首次引擎注册完成后 动态注册窗口
// 首次引擎注册完成后 动态注册窗口,使用实际的构造函数
_registerFinish && WindowManager.dynamicRegisterWindow(ctor, groupName, pkgName, name, bundle || "");
return ctor;
};
}
@@ -110,10 +111,12 @@ export namespace _uidecorator {
* @param {string} name 组件名
*/
export function uicom(pkg: string, name: string): Function {
return function (ctor: any): void {
return function (ctor: any): any {
// 检查是否有原始构造函数引用(由其他装饰器如 @dataclass 提供)
const originalCtor = ctor;
// log(`pkg:【${pkg}】 uicom prop >${JSON.stringify(ctor[UIPropMeta] || {})}<`);
uicomponentMap.set(ctor, {
ctor: ctor,
uicomponentMap.set(originalCtor, {
ctor: ctor, // 存储实际的构造函数(可能被包装过)
props: ctor[UIPropMeta] || null,
callbacks: ctor[UICBMeta] || null,
controls: ctor[UIControlMeta] || null,
@@ -123,8 +126,9 @@ export namespace _uidecorator {
name: name,
}
});
// 首次引擎注册完成后 动态注册自定义组件
// 首次引擎注册完成后 动态注册自定义组件,使用实际的构造函数
_registerFinish && ComponentExtendHelper.dynamicRegister(ctor, pkg, name);
return ctor;
};
}
@@ -155,9 +159,11 @@ export namespace _uidecorator {
*/
export function uiheader(pkg: string, name: string, bundle?: string): Function {
return function (ctor: any): void {
// 检查是否有原始构造函数引用(由其他装饰器如 @dataclass 提供)
const originalCtor = ctor;
// log(`pkg:【${pkg}】 uiheader prop >${JSON.stringify(ctor[UIPropMeta] || {})}<`);
uiheaderMap.set(ctor, {
ctor: ctor,
uiheaderMap.set(originalCtor, {
ctor: ctor, // 存储实际的构造函数(可能被包装过)
props: ctor[UIPropMeta] || null,
callbacks: ctor[UICBMeta] || null,
controls: ctor[UIControlMeta] || null,
@@ -168,7 +174,7 @@ export namespace _uidecorator {
bundle: bundle || "",
}
});
// 首次引擎注册完成后 动态注册窗口header
// 首次引擎注册完成后 动态注册窗口header,使用实际的构造函数
_registerFinish && WindowManager.dynamicRegisterHeader(ctor, pkg, name, bundle || "");
return ctor;
};