diff --git a/assets/libs/model-view/JsonOb.ts b/assets/libs/model-view/JsonOb.ts index 91ddfa3..f174320 100644 --- a/assets/libs/model-view/JsonOb.ts +++ b/assets/libs/model-view/JsonOb.ts @@ -2,7 +2,19 @@ * @Author: dgflash * @Date: 2022-09-01 18:00:28 * @LastEditors: dgflash - * @LastEditTime: 2022-09-06 17:18:05 + * @LastEditTime: 2024-03-08 10:00:00 + * + * JsonOb 性能优化版本(默认实现) + * + * 优化特性: + * - 防止重复观察同一对象 + * - 优化数组操作(只监听新增元素) + * - 支持冻结数据(不监听静态配置) + * - 支持批量更新(减少回调次数) + * - 支持自定义深度限制 + * - 更安全的内存管理 + * + * API 完全兼容原始版本,可零改动升级 */ /** @@ -14,40 +26,93 @@ const types = { obj: '[object Object]', array: '[object Array]' }; -const OAM = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice']; + +/** 冻结标记 */ +const FROZEN_KEY = Symbol('__frozen__'); + +/** 配置选项 */ +export interface JsonObOptions { + /** 最大监听深度,默认 10 */ + maxDepth?: number; + /** 冻结的属性列表(不监听这些属性) */ + frozenKeys?: string[]; + /** 是否启用批量更新,默认 false */ + enableBatch?: boolean; + /** 批量更新延迟(毫秒),默认 16ms(一帧) */ + batchDelay?: number; +} /** - * 实现属性拦截的类 + * 标记对象为冻结状态(不会被监听) + * 适用于静态配置数据,可提升性能 + */ +export function freezeData(obj: any): void { + if (typeof obj === 'object' && obj !== null) { + obj[FROZEN_KEY] = true; + } +} + +/** + * 检查对象是否被冻结 + */ +export function isFrozen(obj: any): boolean { + return obj && obj[FROZEN_KEY] === true; +} + +/** + * 实现属性拦截的类(性能优化版) */ export class JsonOb { - constructor(obj: T, callback: (newVal: any, oldVal: any, pathArray: string[]) => void) { + constructor( + obj: T, + callback: (newVal: any, oldVal: any, pathArray: string[]) => void, + options?: JsonObOptions + ) { if (OP.toString.call(obj) !== types.obj && OP.toString.call(obj) !== types.array) { console.error('请传入一个对象或数组'); } + this._callback = callback; this._root = obj; + this._options = { + maxDepth: 10, + frozenKeys: [], + enableBatch: false, + batchDelay: 16, + ...options + }; this._observedObjects = new WeakSet(); this._overriddenArrays = new WeakMap(); this.observe(obj); } - private _callback; + private _callback: ((newVal: any, oldVal: any, pathArray: string[]) => void) | null; private _root: T; + private _options: Required; private _observedObjects: WeakSet; private _overriddenArrays: WeakMap; private _isDestroyed = false; + // 批量更新相关 + private _pendingChanges = new Map(); + private _batchTimer: any = null; + /** 对象属性劫持 */ private observe(obj: T, path?: any) { if (this._isDestroyed) return; - + + // 检查是否被冻结 + if (isFrozen(obj)) return; + // 防止重复观察同一个对象 if (this._observedObjects.has(obj)) return; this._observedObjects.add(obj); - // 深度限制,防止过深递归(最大深度10) - if (path && path.length > 10) { - console.warn('JsonOb: 对象嵌套深度超过10层,停止监听'); + // 深度限制 + if (path && path.length >= this._options.maxDepth) { + if (path.length === this._options.maxDepth) { + console.warn(`JsonOb: 对象嵌套深度超过${this._options.maxDepth}层,停止监听`); + } return; } @@ -55,110 +120,206 @@ export class JsonOb { this.overrideArrayProto(obj, path); } - // @ts-ignore 注:避免API生成工具报错 + // @ts-ignore Object.keys(obj).forEach((key) => { + // 跳过冻结的属性 + if (this._options.frozenKeys.includes(key)) { + return; + } + const self = this; // @ts-ignore let oldVal = obj[key]; - // 创建路径数组的副本,避免引用问题 const pathArray = path ? [...path, key] : [key]; - + Object.defineProperty(obj, key, { get: function () { return oldVal; }, set: function (newVal) { if (self._isDestroyed) return; - + if (oldVal !== newVal) { - if (OP.toString.call(newVal) === types.obj) { + const ov = oldVal; + oldVal = newVal; + + // 如果新值是对象,继续监听 + if (OP.toString.call(newVal) === types.obj && !isFrozen(newVal)) { self.observe(newVal, pathArray); } - const ov = oldVal; - oldVal = newVal; - // 传递路径数组的副本,防止外部修改 - self._callback(newVal, ov, pathArray.slice()); + // 触发回调 + self.triggerChange(newVal, ov, pathArray); } - } + }, + enumerable: true, + configurable: true }); // @ts-ignore const o = obj[key]; - if (OP.toString.call(o) === types.obj || OP.toString.call(o) === types.array) { + if ((OP.toString.call(o) === types.obj || OP.toString.call(o) === types.array) && !isFrozen(o)) { this.observe(o, pathArray); } }, this); } /** - * 对数组类型进行动态绑定 - * @param array - * @param path + * 对数组类型进行动态绑定(优化版) */ private overrideArrayProto(array: any, path: any) { if (this._isDestroyed) return; - + // 检查是否已经重写过该数组 if (this._overriddenArrays.has(array)) return; - // 保存原始 Array 原型 const originalProto = Array.prototype; - // 通过 Object.create 方法创建一个对象,该对象的原型是Array.prototype const overrideProto = Object.create(Array.prototype); const self = this; - - // 存储原始原型引用,用于后续恢复 + this._overriddenArrays.set(array, originalProto); - // 遍历要重写的数组方法 - OAM.forEach((method: any) => { + // 修改型方法 + const mutatingMethods = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice']; + + mutatingMethods.forEach((method: any) => { Object.defineProperty(overrideProto, method, { value: function () { if (self._isDestroyed) return originalProto[method].apply(this, arguments); - + const oldVal = this.slice(); - // 调用原始原型上的方法 const result = originalProto[method].apply(this, arguments); - // 继续监听新数组 - self.observe(this, path); - // 传递路径数组的副本 - self._callback(this, oldVal, path ? path.slice() : path); + + // 优化:只监听新增的元素 + if (method === 'push' || method === 'unshift') { + const newItems = Array.from(arguments); + const startIndex = method === 'push' ? this.length - newItems.length : 0; + + newItems.forEach((item, index) => { + if ((OP.toString.call(item) === types.obj || OP.toString.call(item) === types.array) && !isFrozen(item)) { + const itemPath = path ? [...path, (startIndex + index).toString()] : [(startIndex + index).toString()]; + self.observe(item, itemPath); + } + }); + } else if (method === 'splice') { + // splice 可能添加新元素 + const startIndex = arguments[0]; + const newItems = Array.prototype.slice.call(arguments, 2); + + newItems.forEach((item, index) => { + if ((OP.toString.call(item) === types.obj || OP.toString.call(item) === types.array) && !isFrozen(item)) { + const itemPath = path ? [...path, (startIndex + index).toString()] : [(startIndex + index).toString()]; + self.observe(item, itemPath); + } + }); + } + + // 触发回调 + self.triggerChange(this, oldVal, path ? path.slice() : path); return result; }, writable: true, - configurable: true + configurable: true, + enumerable: false }); }); - // 使用 Object.setPrototypeOf 代替直接修改 __proto__(更安全) + // 使用 Object.setPrototypeOf 代替直接修改 __proto__ try { Object.setPrototypeOf(array, overrideProto); } catch (e) { - // 降级方案:如果不支持 setPrototypeOf,使用 __proto__ + // 降级方案 array['__proto__'] = overrideProto; } } + /** + * 触发变化回调 + */ + private triggerChange(newVal: any, oldVal: any, pathArray: string[]) { + if (this._options.enableBatch) { + this.queueChange(newVal, oldVal, pathArray); + } else { + this._callback?.(newVal, oldVal, pathArray.slice()); + } + } + + /** + * 批量更新:将变化加入队列 + */ + private queueChange(newVal: any, oldVal: any, pathArray: string[]) { + const pathKey = pathArray.join('.'); + + const existing = this._pendingChanges.get(pathKey); + this._pendingChanges.set(pathKey, { + newVal, + oldVal: existing ? existing.oldVal : oldVal, + path: pathArray + }); + + this.scheduleBatchUpdate(); + } + + /** + * 调度批量更新 + */ + private scheduleBatchUpdate() { + if (this._batchTimer !== null) return; + + this._batchTimer = setTimeout(() => { + this.flushChanges(); + }, this._options.batchDelay); + } + + /** + * 刷新所有待处理的变化 + */ + private flushChanges() { + if (this._isDestroyed || !this._callback) return; + + this._pendingChanges.forEach((change) => { + this._callback!(change.newVal, change.oldVal, change.path); + }); + + this._pendingChanges.clear(); + this._batchTimer = null; + } + + /** + * 立即刷新所有待处理的变化(仅在启用批量更新时有效) + */ + flush() { + if (this._batchTimer !== null) { + clearTimeout(this._batchTimer); + this._batchTimer = null; + } + this.flushChanges(); + } + /** * 销毁监听,释放内存 - * 注意:无法完全恢复属性劫持,但可以停止回调和清理引用 */ destroy() { if (this._isDestroyed) return; - + this._isDestroyed = true; - - // 清空回调引用 - // @ts-ignore - this._callback = null; - - // 尝试恢复数组原型(只能恢复我们记录的) - // 注意:由于 WeakMap 的特性,这里无法遍历所有数组 - // 但当数组被垃圾回收时,WeakMap 会自动清理 - + + // 刷新待处理的变化 + if (this._options.enableBatch) { + this.flush(); + } + + // 清理定时器 + if (this._batchTimer !== null) { + clearTimeout(this._batchTimer); + this._batchTimer = null; + } + // 清空引用 + this._callback = null; // @ts-ignore this._root = null; + this._pendingChanges.clear(); } } +