1/* 2 * Copyright (c) 2022-2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16const OBSERVABLE_TARGET = "__proxy_observable_target__" 17 18export function getObservableTarget(proxy: Object): Object { 19 return getPropertyValue(OBSERVABLE_TARGET, proxy) ?? proxy 20} 21 22function getPropertyValue(name: string, object: any): any { 23 return object[name] 24} 25 26/** 27 * Data class decorator that makes all child fields trackable. 28 */ 29export function Observed(constructorFunction: Function) { 30 constructorFunction.prototype[OBSERVED] = true 31} 32 33const OBSERVED = "__ObservedByArkUI__" 34function isObserved(value: any): boolean { 35 return value[OBSERVED] === true 36} 37 38/** @internal */ 39export interface Observable { 40 /** It is called when the observable value is accessed. */ 41 onAccess(): void 42 /** It is called when the observable value is modified. */ 43 onModify(): void 44} 45 46/** @internal */ 47export class ObservableHandler implements Observable { 48 private static handlers: WeakMap<Object, ObservableHandler> | undefined = undefined 49 50 private parents = new Set<ObservableHandler>() 51 private children = new Map<ObservableHandler, number>() 52 53 private readonly observables = new Set<Observable>() 54 private _modified = false 55 56 readonly observed: boolean 57 constructor(parent?: ObservableHandler, observed: boolean = false) { 58 this.observed = observed 59 if (parent) this.addParent(parent) 60 } 61 62 onAccess(): void { 63 if (this.observables.size > 0) { 64 const it = this.observables.keys() 65 while (true) { 66 const result = it.next() 67 if (result.done) break 68 result.value?.onAccess() 69 } 70 } 71 } 72 73 onModify(): void { 74 const set = new Set<ObservableHandler>() 75 this.collect(true, set) 76 set.forEach((handler: ObservableHandler) => { 77 handler._modified = true 78 if (handler.observables.size > 0) { 79 const it = handler.observables.keys() 80 while (true) { 81 const result = it.next() 82 if (result.done) break 83 result.value?.onModify() 84 } 85 } 86 }) 87 } 88 89 static dropModified<Value>(value: Value): boolean { 90 const handler = ObservableHandler.findIfObject(value) 91 if (handler === undefined) return false 92 const result = handler._modified 93 handler._modified = false 94 return result 95 } 96 97 /** Adds the specified `observable` to the handler corresponding to the given `value`. */ 98 static attach<Value>(value: Value, observable: Observable): void { 99 const handler = ObservableHandler.findIfObject(value) 100 if (handler) handler.observables.add(observable) 101 } 102 103 /** Deletes the specified `observable` from the handler corresponding to the given `value`. */ 104 static detach<Value>(value: Value, observable: Observable): void { 105 const handler = ObservableHandler.findIfObject(value) 106 if (handler) handler.observables.delete(observable) 107 } 108 109 /** @returns the handler corresponding to the given `value` if it was installed */ 110 private static findIfObject<Value>(value: Value): ObservableHandler | undefined { 111 const handlers = ObservableHandler.handlers 112 return handlers !== undefined && value instanceof Object ? handlers.get(getObservableTarget(value as Object)) : undefined 113 } 114 115 /** 116 * @param value - any non-null object including arrays 117 * @returns an observable handler or `undefined` if it is not installed 118 */ 119 static find(value: Object): ObservableHandler | undefined { 120 const handlers = ObservableHandler.handlers 121 return handlers ? handlers.get(getObservableTarget(value)) : undefined 122 } 123 124 /** 125 * @param value - any non-null object including arrays 126 * @param observable - a handler to install on this object 127 * @throws an error if observable handler cannot be installed 128 */ 129 static installOn(value: Object, observable?: ObservableHandler): void { 130 let handlers = ObservableHandler.handlers 131 if (handlers === undefined) { 132 handlers = new WeakMap<Object, ObservableHandler>() 133 ObservableHandler.handlers = handlers 134 } 135 observable 136 ? handlers.set(getObservableTarget(value), observable) 137 : handlers.delete(getObservableTarget(value)) 138 } 139 140 addParent(parent: ObservableHandler) { 141 const count = parent.children.get(this) ?? 0 142 parent.children.set(this, count + 1) 143 this.parents.add(parent) 144 } 145 146 removeParent(parent: ObservableHandler) { 147 const count = parent.children.get(this) ?? 0 148 if (count > 1) { 149 parent.children.set(this, count - 1) 150 } 151 else if (count == 1) { 152 parent.children.delete(this) 153 this.parents.delete(parent) 154 } 155 } 156 157 removeChild<Value>(value: Value) { 158 const child = ObservableHandler.findIfObject(value) 159 if (child) child.removeParent(this) 160 } 161 162 private collect(all: boolean, guards = new Set<ObservableHandler>()) { 163 if (guards.has(this)) return guards // already collected 164 guards.add(this) // handler is already guarded 165 this.parents.forEach(handler => handler.collect(all, guards)) 166 if (all) this.children.forEach((_count, handler) => handler.collect(all, guards)) 167 return guards 168 } 169 170 static contains(observable: ObservableHandler, guards?: Set<ObservableHandler>) { 171 if (observable.observed) return true 172 if (guards === undefined) guards = new Set<ObservableHandler>() // create if needed 173 else if (guards.has(observable)) return false // already checked 174 guards.add(observable) // handler is already guarded 175 for (const it of observable.parents.keys()) { 176 if (ObservableHandler.contains(it, guards)) return true 177 } 178 return false 179 } 180} 181 182/** @internal */ 183export function observableProxyArray<Value>(...value: Value[]): Array<Value> { 184 return observableProxy(value) 185} 186 187/** @internal */ 188export function observableProxy<Value>(value: Value, parent?: ObservableHandler, observed?: boolean, strict = true): Value { 189 if (value instanceof ObservableHandler) return value // do not proxy a marker itself 190 if (value === null || !(value instanceof Object)) return value // only non-null object can be observable 191 const observable = ObservableHandler.find(value) 192 if (observable) { 193 if (parent) { 194 if (strict) observable.addParent(parent) 195 if (observed === undefined) observed = ObservableHandler.contains(parent) 196 } 197 if (observed) { 198 if (Array.isArray(value)) { 199 for (let index = 0; index < value.length; index++) { 200 value[index] = observableProxy(value[index], observable, observed, false) 201 } 202 } else { 203 proxyFields(value, false, observable) 204 } 205 } 206 return value 207 } 208 if (Array.isArray(value)) { 209 const handler = new ObservableHandler(parent) 210 const array = proxyChildrenOnly(value, handler, observed) 211 copyWithinObservable(array) 212 fillObservable(array) 213 popObservable(array) 214 pushObservable(array) 215 reverseObservable(array) 216 shiftObservable(array) 217 sortObservable(array) 218 spliceObservable(array) 219 unshiftObservable(array) 220 return proxyObject(array, handler) 221 } 222 if (value instanceof Date) { 223 const valueAsAny = (value as any) 224 const handler = new ObservableHandler(parent) 225 const setMethods = new Set([ 226 "setFullYear", "setMonth", "setDate", "setHours", "setMinutes", "setSeconds", 227 "setMilliseconds", "setTime", "setUTCFullYear", "setUTCMonth", "setUTCDate", 228 "setUTCHours", "setUTCMinutes", "setUTCSeconds", "setUTCMilliseconds" 229 ]) 230 setMethods.forEach((method: string) => { 231 const originalMethod = method + 'Original' 232 if (valueAsAny[originalMethod] !== undefined) { 233 return 234 } 235 valueAsAny[originalMethod] = valueAsAny[method] 236 valueAsAny[method] = function (...args: any[]) { 237 ObservableHandler.find(this)?.onModify() 238 return this[originalMethod](...args) 239 } 240 }) 241 return proxyObject(value, handler) 242 } 243 // TODO: support set/map 244 const handler = new ObservableHandler(parent, isObserved(value)) 245 if (handler.observed || observed) proxyFields(value, true, handler) 246 return proxyObject(value, handler) 247} 248 249function proxyObject(value: any, observable: ObservableHandler) { 250 ObservableHandler.installOn(value, observable) 251 return new Proxy(value, { 252 get(target, property, receiver) { 253 if (property == OBSERVABLE_TARGET) return target 254 const value: any = Reflect.get(target, property, receiver) 255 ObservableHandler.find(target)?.onAccess() 256 return typeof value == "function" 257 ? value.bind(target) 258 : value 259 }, 260 set(target, property, value, receiver) { 261 const old = Reflect.get(target, property, receiver) 262 if (value === old) return true 263 const observable = ObservableHandler.find(target) 264 if (observable) { 265 observable.onModify() 266 observable.removeChild(old) 267 const observed = ObservableHandler.contains(observable) 268 if (observed || Array.isArray(target)) { 269 value = observableProxy(value, observable, observed) 270 } 271 } 272 return Reflect.set(target, property, value, receiver) 273 }, 274 deleteProperty(target, property) { 275 ObservableHandler.find(target)?.onModify() 276 delete target[property] 277 return true 278 }, 279 }) 280} 281 282function proxyFields(value: any, strict: boolean, parent?: ObservableHandler) { 283 for (const name of Object.getOwnPropertyNames(value)) { 284 const descriptor = Object.getOwnPropertyDescriptor(value, name) 285 if (descriptor?.writable) value[name] = observableProxy(value[name], parent, true, strict) 286 } 287} 288 289function proxyChildrenOnly(array: any[], parent: ObservableHandler, observed?: boolean): any[] { 290 if (observed === undefined) observed = ObservableHandler.contains(parent) 291 return array.map(it => observableProxy(it, parent, observed)) 292} 293 294function copyWithinObservable(array: any) { 295 if (array.copyWithinOriginal === undefined) { 296 array.copyWithinOriginal = array.copyWithin 297 array.copyWithin = function (this, target: number, start: number, end?: number) { 298 const observable = ObservableHandler.find(this) 299 observable?.onModify() 300 return this.copyWithinOriginal(target, start, end) 301 } 302 } 303} 304 305function fillObservable(array: any) { 306 if (array.fillOriginal === undefined) { 307 array.fillOriginal = array.fill 308 array.fill = function (this, value: any, start?: number, end?: number) { 309 const observable = ObservableHandler.find(this) 310 observable?.onModify() 311 if (observable) value = observableProxy(value, observable) 312 return this.fillOriginal(value, start, end) 313 } 314 } 315} 316 317function popObservable(array: any) { 318 if (array.popOriginal === undefined) { 319 array.popOriginal = array.pop 320 array.pop = function (...args: any[]) { 321 const observable = ObservableHandler.find(this) 322 observable?.onModify() 323 const result = this.popOriginal(...args) 324 if (observable) observable.removeChild(result) 325 return result 326 } 327 } 328} 329 330function pushObservable(array: any) { 331 if (array.pushOriginal === undefined) { 332 array.pushOriginal = array.push 333 array.push = function (this, ...args: any[]) { 334 const observable = ObservableHandler.find(this) 335 observable?.onModify() 336 if (observable) args = proxyChildrenOnly(args, observable) 337 return this.pushOriginal(...args) 338 } 339 } 340} 341 342function reverseObservable(array: any) { 343 if (array.reverseOriginal === undefined) { 344 array.reverseOriginal = array.reverse 345 array.reverse = function (this) { 346 const observable = ObservableHandler.find(this) 347 observable?.onModify() 348 return this.reverseOriginal() 349 } 350 } 351} 352 353function shiftObservable(array: any) { 354 if (array.shiftOriginal === undefined) { 355 array.shiftOriginal = array.shift 356 array.shift = function (this, ...args: any[]) { 357 const observable = ObservableHandler.find(this) 358 observable?.onModify() 359 const result = this.shiftOriginal(...args) 360 if (observable) observable.removeChild(result) 361 return result 362 } 363 } 364} 365 366function sortObservable(array: any) { 367 if (array.sortOriginal === undefined) { 368 array.sortOriginal = array.sort 369 array.sort = function (this, compareFn?: (a: any, b: any) => number) { 370 const observable = ObservableHandler.find(this) 371 observable?.onModify() 372 return this.sortOriginal(compareFn) 373 } 374 } 375} 376 377function spliceObservable(array: any) { 378 if (array.spliceOriginal === undefined) { 379 array.spliceOriginal = array.splice 380 array.splice = function (this, start: number, deleteCount: number, ...items: any[]) { 381 const observable = ObservableHandler.find(this) 382 observable?.onModify() 383 if (observable) items = proxyChildrenOnly(items, observable) 384 if (deleteCount === undefined) deleteCount = array.length 385 const result = this.spliceOriginal(start, deleteCount, ...items) 386 if (observable && Array.isArray(result)) { 387 result.forEach(it => observable.removeChild(it)) 388 } 389 return result 390 } 391 } 392} 393 394function unshiftObservable(array: any) { 395 if (array.unshiftOriginal === undefined) { 396 array.unshiftOriginal = array.unshift 397 array.unshift = function (this, ...items: any[]) { 398 const observable = ObservableHandler.find(this) 399 observable?.onModify() 400 if (observable) items = proxyChildrenOnly(items, observable) 401 return this.unshiftOriginal(...items) 402 } 403 } 404} 405