1/* 2 * Copyright (c) 2024 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 16 17/** 18 * 19 * This file includes only framework internal classes and functions 20 * non are part of SDK. Do not access from app. 21 * 22 * 23 * ObserveV2 is the singleton object for observing state variable access and 24 * change 25 */ 26 27// stackOfRenderedComponentsItem[0] and stackOfRenderedComponentsItem[1] is faster than 28// the stackOfRenderedComponentsItem.id and the stackOfRenderedComponentsItem.cmp. 29// So use the array to keep id and cmp. 30type StackOfRenderedComponentsItem = [number, MonitorV2 | ComputedV2 | PersistenceV2Impl | ViewBuildNodeBase]; 31 32// in the case of ForEach, Repeat, AND If, two or more UINodes / elementIds can render at the same time 33// e.g. ForEach -> ForEach child Text, Repeat -> Nested Repeat, child Text 34// Therefore, ObserveV2 needs to keep a stack of currently rendering ids / components 35// in the same way as this is also done for PU stateMgmt with ViewPU.currentlyRenderedElmtIdStack_ 36class StackOfRenderedComponents { 37 private stack_: Array<StackOfRenderedComponentsItem> = new Array<StackOfRenderedComponentsItem>(); 38 39 public push(id: number, cmp: MonitorV2 | ComputedV2 | PersistenceV2Impl | ViewBuildNodeBase): void { 40 this.stack_.push([id, cmp]); 41 } 42 43 public pop(): StackOfRenderedComponentsItem | undefined { 44 return this.stack_.pop(); 45 } 46 47 public top(): StackOfRenderedComponentsItem | undefined { 48 return this.stack_.length ? this.stack_[this.stack_.length - 1] : undefined; 49 } 50} 51 52class ObserveV2 { 53 // meta data about decorated variable inside prototype 54 public static readonly V2_DECO_META = Symbol('__v2_deco_meta__'); 55 56 // meta data about decorated accessor and method: @Computed and @Monitor 57 public static readonly V2_DECO_METHOD_META = Symbol('__v2_deco_method_meta__'); 58 59 public static readonly SYMBOL_REFS = Symbol('__use_refs__'); 60 public static readonly ID_REFS = Symbol('__id_refs__'); 61 public static readonly MONITOR_REFS = Symbol('___monitor_refs_'); 62 public static readonly ADD_MONITOR_REFS = Symbol('___monitor_refs_'); 63 public static readonly COMPUTED_REFS = Symbol('___computed_refs_'); 64 65 public static readonly SYMBOL_PROXY_GET_TARGET = Symbol('__proxy_get_target'); 66 67 public static readonly SYMBOL_MAKE_OBSERVED = Symbol('___make_observed__'); 68 69 public static readonly OB_PREFIX = '__ob_'; // OB_PREFIX + attrName => backing store attribute name 70 public static readonly OB_PREFIX_LEN = 5; 71 public static readonly NO_REUSE = -1; // mark no reuse on-going 72 // used by array Handler to create dependency on artificial 'length' 73 // property of array, mark it as changed when array has changed. 74 public static readonly OB_LENGTH = '___obj_length'; 75 76 private static setMapProxy: SetMapProxyHandler = new SetMapProxyHandler(); 77 private static arrayProxy: ArrayProxyHandler = new ArrayProxyHandler(); 78 private static objectProxy: ObjectProxyHandler = new ObjectProxyHandler(); 79 80 // see MonitorV2.observeObjectAccess: bindCmp is the MonitorV2 81 // see modified ViewV2 and ViewPU observeComponentCreation, bindCmp is the ViewV2 or ViewPU 82 83 // bindId: UINode elmtId or watchId, depending on what is being observed 84 private stackOfRenderedComponents_: StackOfRenderedComponents = new StackOfRenderedComponents(); 85 86 // Map bindId to WeakRef<ViewBuildNodeBase> 87 public id2cmp_: { number: WeakRef<ViewBuildNodeBase> } = {} as { number: WeakRef<ViewBuildNodeBase> }; 88 89 // Map bindId to WeakRef<MonitorV2 | ComputedV2 | PersistenceV2Impl> 90 public id2Others_: { number: WeakRef<MonitorV2 | ComputedV2 | PersistenceV2Impl> } = {} as { number: WeakRef<MonitorV2 | ComputedV2 | PersistenceV2Impl> }; 91 92 // Map bindId -> Set of @ObservedV2 class objects 93 // reverse dependency map for quickly removing all dependencies of a bindId 94 private id2targets_: { number: Set<WeakRef<Object>> } = {} as { number: Set<WeakRef<Object>> }; 95 96 // Queue of tasks to run in next idle period (used for optimization) 97 public idleTasks_: (Array<[(...any: any[]) => any, ...any[]]> & { first: number, end: number }) = 98 Object.assign(Array(1000).fill([]), { first: 0, end: 0 }); 99 public static readonly idleTasksInitLength = 1000; 100 101 // queued up Set of bindId 102 // elmtIds of UINodes need re-render 103 // @monitor functions that need to execute 104 public elmtIdsChanged_: Set<number> = new Set(); 105 // @Computed id 106 private computedPropIdsChanged_: Set<number> = new Set(); 107 // AddMonitor API 108 private monitorIdsChangedForAddMonitor_: Set<number> = new Set(); 109 // Sync AddMonitor API 110 private monitorSyncIdsChangedForAddMonitor_: Set<number> = new Set(); 111 // @Monitor id 112 private monitorIdsChanged_: Set<number> = new Set(); 113 private persistenceChanged_: Set<number> = new Set(); 114 // used for Monitor API 115 // only store the MonitorV2 id, not the path id 116 // to make sure the callback function will be executed only once 117 public monitorFuncsToRun_: Set<number> = new Set(); 118 119 // ViewV2s Grouped by instance id (container id) 120 private viewV2NeedUpdateMap_: Map<number, Map<ViewV2 | ViewPU, Array<number>>> = new Map(); 121 122 // To avoid multiple schedules on the same container 123 private scheduledContainerIds_: Set<number> = new Set(); 124 125 // avoid recursive execution of updateDirty 126 // by state changes => fireChange while 127 // UINode rerender or @monitor function execution 128 private startDirty_: boolean = false; 129 130 // flag to indicate change observation is disabled 131 private disabled_: boolean = false; 132 // flag to indicate dependency recording is disabled 133 private disableRecording_: boolean = false; 134 135 // flag to indicate ComputedV2 calculation is ongoing 136 private calculatingComputedProp_: boolean = false; 137 138 // use for mark current reuse id, ObserveV2.NO_REUSE(-1) mean no reuse on-going 139 protected currentReuseId_: number = ObserveV2.NO_REUSE; 140 141 // flag to disable nested component optimization if V1 and V2 components are involved in the nested cases. 142 public isParentChildOptimizable_ : boolean = true; 143 144 private static obsInstance_: ObserveV2; 145 146 public static getObserve(): ObserveV2 { 147 if (!this.obsInstance_) { 148 this.obsInstance_ = new ObserveV2(); 149 } 150 return this.obsInstance_; 151 } 152 153 // return true given value is @ObservedV2 object 154 public static IsObservedObjectV2(value: any): boolean { 155 return (value && typeof (value) === 'object' && value[ObserveV2.V2_DECO_META]); 156 } 157 158 // return true if given value is proxied observed object, either makeObserved or autoProxyObject 159 public static IsProxiedObservedV2(value: any): boolean { 160 return (value && typeof value === 'object' && value[ObserveV2.SYMBOL_PROXY_GET_TARGET]); 161 } 162 163 // return true given value is the return value of makeObserved 164 public static IsMakeObserved(value: any): boolean { 165 return (value && typeof (value) === 'object' && value[ObserveV2.SYMBOL_MAKE_OBSERVED]); 166 } 167 168 public static getCurrentRecordedId(): number { 169 const bound = ObserveV2.getObserve().stackOfRenderedComponents_.top(); 170 return bound ? bound[0] : -1; 171 } 172 173 // At the start of observeComponentCreation or 174 // MonitorV2 observeObjectAccess 175 public startRecordDependencies(cmp: MonitorV2 | ComputedV2 | PersistenceV2Impl | ViewBuildNodeBase, id: number, doClearBinding: boolean = true): void { 176 if (cmp != null) { 177 doClearBinding && this.clearBinding(id); 178 this.stackOfRenderedComponents_.push(id, cmp); 179 } 180 } 181 182 // At the start of observeComponentCreation or 183 // MonitorV2 observeObjectAccess 184 public stopRecordDependencies(): void { 185 const bound = this.stackOfRenderedComponents_.pop(); 186 if (bound === undefined) { 187 stateMgmtConsole.error('stopRecordDependencies finds empty stack. Internal error!'); 188 return; 189 } 190 191 // only add IView | MonitorV2 | ComputedV2 if at least one dependency was 192 // recorded when rendering this ViewPU/ViewV2/Monitor/ComputedV2 193 // ViewPU is the likely case where no dependency gets recorded 194 // for others no dependencies are unlikely to happen 195 196 // once set, the value remains unchanged 197 let id: number = bound[0]; 198 let cmp: MonitorV2 | ComputedV2 | PersistenceV2Impl | ViewBuildNodeBase = bound[1]; 199 200 // element id can be registered from id2cmp in aboutToBeDeletedInternal and unregisterElmtIdsFromIViews 201 // PersistenceV2Impl instance is Singleton 202 if (cmp instanceof ViewBuildNodeBase || cmp instanceof PersistenceV2Impl) { 203 this.id2cmp_[id] = new WeakRef<ViewBuildNodeBase | PersistenceV2Impl>(cmp); 204 return; 205 } 206 const weakRef = WeakRefPool.get(cmp); 207 // this instance, which maybe MonitorV2/ComputedV2 have been already recorded in id2Others 208 if (this.id2Others_[id] === weakRef) { 209 return; 210 } 211 this.id2Others_[id] = weakRef; 212 // register MonitorV2/ComputedV2 instance gc-callback func 213 WeakRefPool.register(cmp, id, () => { 214 delete this.id2Others_[id]; 215 }); 216 217 } 218 219 // clear any previously created dependency view model object to elmtId 220 // find these view model objects with the reverse map id2targets_ 221 public clearBinding(id: number): void { 222 if (this.idleTasks_) { 223 if (this.idleTasks_.end - this.idleTasks_.first > ObserveV2.idleTasksInitLength) { 224 ObserveV2.getObserve().runIdleTasks(); 225 } 226 this.idleTasks_[this.idleTasks_.end++] = [this.clearBindingInternal, id]; 227 } else { 228 this.clearBindingInternal(id); 229 } 230 } 231 232 private clearBindingInternal(id: number): void { 233 this.id2targets_[id]?.forEach((weakRef: WeakRef<Object>) => { 234 const target = weakRef.deref(); 235 const idRefs: Object | undefined = target?.[ObserveV2.ID_REFS]; 236 const symRefs: Object = target?.[ObserveV2.SYMBOL_REFS]; 237 238 if (idRefs) { 239 idRefs[id]?.forEach(key => 240 symRefs?.[key]?.delete(id) 241 ); 242 delete idRefs[id]; 243 } else { 244 for (let key in symRefs) { 245 symRefs[key]?.delete(id); 246 }; 247 } 248 249 if (target) { 250 WeakRefPool.unregister(target, id); 251 } 252 }); 253 254 delete this.id2targets_[id]; 255 256 stateMgmtConsole.propertyAccess(`... id2targets_ length=${Object.keys(this.id2targets_).length}, entries=${JSON.stringify(Object.keys(this.id2targets_))} `); 257 } 258 259 /** 260 * 261 * According to JS specifications, it is up to ArlTS runtime GC implementation when to collect unreferences objects. 262 * Parameters such as available memory, ArkTS processing load, number and size of all JS objects for GC collection 263 * can impact the time delay between an object loosing last reference and GC collecting this object. 264 * 265 * WeakRef deref() returns the object until GC has collected it. 266 * The id2cmp and is2targets cleanup herein depends on WeakRef.deref() to return undefined, i.e. it depends on GC 267 * collecting 'cmp' or 'target' objects. Only then the algorithm can remove the entry from id2cmp / from id2target. 268 * It is therefore to be expected behavior that these map objects grow and they a contain a larger number of 269 * MonitorV2, ComputedV2, and/or view model @Observed class objects that are no longer used / referenced by the application. 270 * Only after ArkTS runtime GC has collected them, this function is able to clean up the id2cmp and is2targets. 271 * 272 */ 273 274 // runs idleTasks until empty or deadline is reached 275 public runIdleTasks(deadline: number = Infinity): void { 276 stateMgmtConsole.debug(`UINodeRegisterProxy.runIdleTasks(${deadline})`); 277 278 // fast check for early return 279 if (!this.idleTasks_ || this.idleTasks_.end === 0) { 280 return; 281 } 282 283 while (this.idleTasks_.first < this.idleTasks_.end) { 284 const [func, ...args] = this.idleTasks_[this.idleTasks_.first] || []; 285 func?.apply(this, args); 286 delete this.idleTasks_[this.idleTasks_.first]; 287 this.idleTasks_.first++; 288 // ensure that there is no accumulation in idleTask leading to oom 289 if (this.idleTasks_.end - this.idleTasks_.first < ObserveV2.idleTasksInitLength && 290 this.idleTasks_.first % 100 === 0 && Date.now() >= deadline - 1) { 291 return; 292 } 293 } 294 this.idleTasks_.first = 0; 295 this.idleTasks_.end = 0; 296 this.idleTasks_.length = ObserveV2.idleTasksInitLength; 297 } 298 299 /** 300 * counts number of WeakRef<Object> entries in id2cmp_ 'map' object 301 * @returns total count and count of WeakRefs that can be deref'ed 302 * Methods only for testing 303 */ 304 public get id2CompDeRefSize(): [totalCount: number, aliveCount: number] { 305 let totalCount = 0; 306 let aliveCount = 0; 307 let comp: Object; 308 for (const id in this.id2cmp_) { 309 totalCount++; 310 let weakRef: WeakRef<Object> = this.id2cmp_[id]; 311 if (weakRef && 'deref' in weakRef && (comp = weakRef.deref()) && comp instanceof Object) { 312 aliveCount++; 313 } 314 } 315 return [totalCount, aliveCount]; 316 } 317 318 /** 319 * counts number of target WeakRef<object> entries in all the Sets inside id2targets 'map' object 320 * @returns total count and those can be dereferenced 321 * Methods only for testing 322 */ 323 public get id2TargetsDerefSize(): [totalCount: number, aliveCount: number] { 324 let totalCount = 0; 325 let aliveCount = 0; 326 for (const id in this.id2targets_) { 327 const targetSet: Set<WeakRef<Object>> | undefined = this.id2targets_[id]; 328 if (targetSet && targetSet instanceof Set) { 329 for (let weakTarget of targetSet) { 330 totalCount++; 331 if (weakTarget.deref()) { 332 aliveCount++; 333 } 334 } // for targetSet 335 } 336 } // for id2targets_ 337 return [totalCount, aliveCount]; 338 } 339 340 // add dependency view model object 'target' property 'attrName' 341 // to current this.bindId 342 public addRef(target: object, attrName: string): void { 343 const bound = this.stackOfRenderedComponents_.top(); 344 if (!bound || this.disableRecording_) { 345 return; 346 } 347 if (bound[0] === UINodeRegisterProxy.monitorIllegalV1V2StateAccess) { 348 const error = `${attrName}: ObserveV2.addRef: trying to use V2 state '${attrName}' to init/update child V2 @Component. Application error`; 349 stateMgmtConsole.applicationError(error); 350 throw new TypeError(error); 351 } 352 353 stateMgmtConsole.propertyAccess(`ObserveV2.addRef '${attrName}' for id ${bound[0]}...`); 354 355 // run in idle time or now 356 if (this.idleTasks_) { 357 this.idleTasks_[this.idleTasks_.end++] = 358 [this.addRef4IdInternal, bound[0], target, attrName]; 359 } else { 360 this.addRef4IdInternal(bound[0], target, attrName); 361 } 362 } 363 364 // add dependency view model object 'target' property 'attrName' to current this.bindId 365 // this variation of the addRef function is only used to record read access to V1 observed object with enableV2Compatibility enabled 366 // e.g. only from within ObservedObject proxy handler implementations. 367 public addRefV2Compatibility(target: object, attrName: string): void { 368 const bound = this.stackOfRenderedComponents_.top(); 369 if (bound && bound[1]) { 370 if (!(bound[1] instanceof ViewPU)) { 371 if (bound[0] === UINodeRegisterProxy.monitorIllegalV1V2StateAccess) { 372 const error = `${attrName}: ObserveV2.addRefV2Compatibility: trying to use V2 state '${attrName}' to init/update child V2 @Component. Application error`; 373 stateMgmtConsole.applicationError(error); 374 throw new TypeError(error); 375 } 376 stateMgmtConsole.propertyAccess(`ObserveV2.addRefV2Compatibility '${attrName}' for id ${bound[0]}...`); 377 this.addRef4Id(bound[0], target, attrName); 378 } else { 379 // inside ViewPU 380 stateMgmtConsole.propertyAccess(`ObserveV2.addRefV2Compatibility '${attrName}' for id ${bound[0]} -- skip addRef because render/update is inside V1 ViewPU`); 381 } 382 } 383 } 384 385 public addRef4Id(id: number, target: object, attrName: string): void { 386 stateMgmtConsole.propertyAccess(`ObserveV2.addRef4Id '${attrName}' for id ${id} ...`); 387 388 // run in idle time or now 389 if (this.idleTasks_) { 390 this.idleTasks_[this.idleTasks_.end++] = 391 [this.addRef4IdInternal, id, target, attrName]; 392 } else { 393 this.addRef4IdInternal(id, target, attrName); 394 } 395 } 396 397 private addRef4IdInternal(id: number, target: object, attrName: string): void { 398 // Map: attribute/symbol -> dependent id 399 const symRefs = target[ObserveV2.SYMBOL_REFS] ??= {}; 400 symRefs[attrName] ??= new Set(); 401 symRefs[attrName].add(id); 402 403 // Map id -> attribute/symbol 404 // optimization for faster clearBinding 405 const idRefs = target[ObserveV2.ID_REFS]; 406 if (idRefs) { 407 idRefs[id] ??= new Set(); 408 idRefs[id].add(attrName); 409 } 410 411 const weakRef = WeakRefPool.get(target); 412 if (this.id2targets_?.[id]?.has(weakRef)) { 413 return; 414 } 415 416 this.id2targets_[id] ??= new Set<WeakRef<Object>>(); 417 this.id2targets_[id].add(weakRef); 418 WeakRefPool.register(target, id, () => { 419 if (this.id2targets_?.[id]?.delete(weakRef) && this.id2targets_[id].size === 0) { 420 delete this.id2targets_[id]; 421 } 422 }); 423 } 424 425 /** 426 * 427 * @param target set tracked attribute to new value without notifying the change 428 * !! use with caution !! 429 * @param attrName 430 * @param newValue 431 */ 432 public setUnmonitored<Z>(target: object, attrName: string, newValue: Z): void { 433 const storeProp = ObserveV2.OB_PREFIX + attrName; 434 if (storeProp in target) { 435 // @Track attrName 436 stateMgmtConsole.propertyAccess(`setUnmonitored '${attrName}' - tracked but unchanged. Doing nothing.`); 437 target[storeProp] = newValue; 438 } else { 439 stateMgmtConsole.propertyAccess(`setUnmonitored '${attrName}' - untracked, assigning straight.`); 440 // untracked attrName 441 target[attrName] = newValue; 442 } 443 } 444 445 /** 446 * Execute given task while state change observation is disabled 447 * A state mutation caused by the task will NOT trigger UI rerender 448 * and @monitor function execution. 449 * 450 * !!! Use with Caution !!! 451 * 452 * @param task a function to execute without monitoring state changes 453 * @returns task function return value 454 */ 455 public executeUnobserved<Z>(task: () => Z): Z { 456 stateMgmtConsole.propertyAccess(`executeUnobserved - start`); 457 this.disabled_ = true; 458 let ret: Z; 459 try { 460 ret = task(); 461 } catch (e) { 462 stateMgmtConsole.applicationError(`executeUnobserved - task execution caused error ${e} !`); 463 } 464 this.disabled_ = false; 465 stateMgmtConsole.propertyAccess(`executeUnobserved - done`); 466 return ret; 467 } 468 469 /** 470 * Execute given task while state dependency recording is disabled 471 * Any property read during task execution will not be recorded as 472 * a dependency 473 * 474 * !!! Use with Caution !!! 475 * 476 * @param task a function to execute without monitoring state changes 477 * @param unobserved flag to disable also change observation 478 * @returns task function return value 479 */ 480 public executeUnrecorded<Z>(task: () => Z, unobserved: boolean = false): Z { 481 stateMgmtConsole.propertyAccess(`executeUnrecorded - start`); 482 this.disableRecording_ = true; 483 // store current value of disabled_ so that we can later restore it properly 484 const oldDisabledValue = this.disabled_; 485 unobserved && (this.disabled_ = true); 486 let ret: Z = task(); 487 this.disableRecording_ = false; 488 unobserved && (this.disabled_ = oldDisabledValue); 489 stateMgmtConsole.propertyAccess(`executeUnrecorded - done`); 490 return ret; 491 } 492 493 494 495 /** 496 * mark view model object 'target' property 'attrName' as changed 497 * notify affected watchIds and elmtIds but exclude given elmtIds 498 * 499 * @param propName ObservedV2 or ViewV2 target 500 * @param attrName attrName 501 * @param ignoreOnProfile The data reported to the profiler needs to be the changed state data. 502 * If the fireChange is invoked before the data changed, it needs to be ignored on the profiler. 503 * The default value is false. 504 */ 505 public fireChange(target: object, attrName: string, excludeElmtIds?: Set<number>, 506 ignoreOnProfiler: boolean = false): void { 507 // forcibly run idle time tasks if any 508 if (this.idleTasks_?.end) { 509 this.runIdleTasks(); 510 } 511 // enable to get more fine grained traces 512 // including 2 (!) .end calls. 513 514 if (!target[ObserveV2.SYMBOL_REFS] || this.disabled_) { 515 return; 516 } 517 518 const bound = this.stackOfRenderedComponents_.top(); 519 if (this.calculatingComputedProp_) { 520 const prop = bound ? (bound[1] as ComputedV2).getProp() : 'unknown computed property'; 521 const error = `Usage of ILLEGAL @Computed function detected for ${prop}! The @Computed function MUST NOT change the state of any observed state variable!`; 522 stateMgmtConsole.applicationError(error); 523 throw new Error(error); 524 } 525 526 // enable this trace marker for more fine grained tracing of the update pipeline 527 // note: two (!) end markers need to be enabled 528 let changedIdSet = target[ObserveV2.SYMBOL_REFS][attrName]; 529 if (changedIdSet instanceof Set === false) { 530 return; 531 } 532 533 stateMgmtConsole.propertyAccess(`ObserveV2.fireChange '${attrName}' dependent ids: ${JSON.stringify(Array.from(changedIdSet))} ...`); 534 535 for (const id of changedIdSet) { 536 // Cannot fireChange the object that is being created. 537 if (bound && id === bound[0]) { 538 continue; 539 } 540 541 // exclude given elementIds 542 if (excludeElmtIds?.has(id)) { 543 stateMgmtConsole.propertyAccess(`... exclude id ${id}`); 544 continue; 545 } 546 547 // if this is the first id to be added to any Set of changed ids, 548 // schedule an 'updateDirty' task 549 // that will run after the current call stack has unwound. 550 // purpose of check for startDirty_ is to avoid going into recursion. This could happen if 551 // exec a re-render or exec a monitor function changes some state -> calls fireChange -> ... 552 const hasPendingChanges = (this.elmtIdsChanged_.size + this.monitorIdsChanged_.size + this.computedPropIdsChanged_.size) > 0; 553 const isReuseInProgress = (this.currentReuseId_ !== ObserveV2.NO_REUSE); 554 const shouldUpdateDirty = (!hasPendingChanges && !this.startDirty_ && !isReuseInProgress); 555 556 if (shouldUpdateDirty) { 557 Promise.resolve().then(this.updateDirty.bind(this)) 558 .catch(error => { 559 stateMgmtConsole.applicationError(`Exception occurred during the update process involving @Computed properties, @Monitor functions or UINode re-rendering`, error); 560 _arkUIUncaughtPromiseError(error); 561 }); 562 } 563 564 // add bindId to the correct Set of pending changes. 565 if (id < ComputedV2.MIN_COMPUTED_ID) { 566 this.elmtIdsChanged_.add(id); 567 } else if (id < MonitorV2.MIN_WATCH_ID) { 568 this.computedPropIdsChanged_.add(id); 569 } else if (id < MonitorV2.MIN_WATCH_FROM_API_ID) { 570 this.monitorIdsChanged_.add(id); 571 } else if (id < MonitorV2.MIN_SYNC_WATCH_FROM_API_ID) { 572 this.monitorIdsChangedForAddMonitor_.add(id); 573 } else if (id < PersistenceV2Impl.MIN_PERSISTENCE_ID) { 574 this.monitorSyncIdsChangedForAddMonitor_.add(id); 575 } else { 576 this.persistenceChanged_.add(id); 577 } 578 } // for 579 580 // execute the AddMonitor synchronous function 581 while (this.monitorSyncIdsChangedForAddMonitor_.size + this.monitorFuncsToRun_.size > 0) { 582 if (this.monitorSyncIdsChangedForAddMonitor_.size) { 583 stateMgmtConsole.debug(`AddMonitor API monitorSyncIdsChangedForAddMonitor_ ${this.monitorSyncIdsChangedForAddMonitor_.size}`) 584 const monitorId: Set<number> = this.monitorSyncIdsChangedForAddMonitor_; 585 this.monitorSyncIdsChangedForAddMonitor_ = new Set<number>(); 586 // update the value and dependency for each path and get the MonitorV2 id needs to be execute 587 this.updateDirtyMonitorPath(monitorId); 588 } 589 if (this.monitorFuncsToRun_.size) { 590 const monitorFuncs = this.monitorFuncsToRun_; 591 this.monitorFuncsToRun_ = new Set<number>(); 592 this.runMonitorFunctionsForAddMonitor(monitorFuncs); 593 } 594 } 595 596 // report the stateVar changed when recording the profiler 597 if (stateMgmtDFX.enableProfiler && !ignoreOnProfiler) { 598 stateMgmtDFX.reportStateInfoToProfilerV2(target, attrName, changedIdSet); 599 } 600 } 601 602 /** 603 * Group elementIds by their containerId (instanceId). 604 * Make sure view.getInstanceId() is called only once for each view., not for all elmtIds!!! 605 */ 606 private groupElementIdsByContainer(): void { 607 stateMgmtConsole.debug('ObserveV2.groupElementIdsByContainer'); 608 609 // Create sorted array and clear set 610 const elmtIds = Array.from(this.elmtIdsChanged_, Number).sort((a, b) => a - b); 611 this.elmtIdsChanged_.clear(); 612 613 // Cache for view -> instanceId 614 const viewInstanceIdCache = new Map<ViewV2 | ViewPU, number>(); 615 616 for (const elmtId of elmtIds) { 617 const view = this.id2cmp_[elmtId]?.deref(); 618 619 // Early continue for invalid views 620 if (!(view instanceof ViewV2 || view instanceof ViewPU)) { 621 continue; 622 } 623 624 // Get or cache instanceId 625 let instanceId = viewInstanceIdCache.get(view); 626 if (instanceId === undefined) { 627 instanceId = view.getInstanceId(); 628 viewInstanceIdCache.set(view, instanceId); 629 } 630 631 // Get or create viewMap 632 let viewMap = this.viewV2NeedUpdateMap_.get(instanceId); 633 if (!viewMap) { 634 viewMap = new Map<ViewV2 | ViewPU, Array<number>>(); 635 this.viewV2NeedUpdateMap_.set(instanceId, viewMap); 636 } 637 638 // Get or create view's element array 639 let elements = viewMap.get(view); 640 if (!elements) { 641 elements = []; 642 viewMap.set(view, elements); 643 } 644 645 elements.push(elmtId); 646 stateMgmtConsole.debug(`groupElementIdsByContainer: elmtId=${elmtId}, view=${view.constructor.name}, instanceId=${instanceId}`); 647 } 648} 649 public updateDirty(): void { 650 this.startDirty_ = true; 651 this.isParentChildOptimizable_ ? this.updateDirty2Optimized(): this.updateDirty2(false); 652 this.startDirty_ = false; 653 } 654 655 /** 656 * Optimized version of updateDirty2 657 * execute /update in this order 658 * - @Computed variables 659 * - @Monitor functions 660 * Request a frame update and schedule a callback to trigger on the next VSync update 661 */ 662 public updateDirty2Optimized(): void { 663 stateMgmtConsole.debug(`ObservedV2.updateDirty2Optimized() start`); 664 // Calling these functions to retain the original behavior 665 // Obtain and unregister the removed elmtIds 666 UINodeRegisterProxy.obtainDeletedElmtIds(); 667 UINodeRegisterProxy.unregisterElmtIdsFromIViews(); 668 669 this.updateComputedAndMonitors(); 670 671 if (this.elmtIdsChanged_.size === 0) { 672 stateMgmtConsole.debug(`Vsync request is unnecessary when no elements have changed - returning from updateDirty2Optimized`); 673 return; 674 } 675 676 // Group elementIds before scheduling update 677 this.groupElementIdsByContainer(); 678 679 // At this point, we have the viewV2NeedUpdateMap_ populated with the ViewV2/elementIds that need update 680 // For each containerId (instance/container) in the map, schedule an update. 681 for (const containerId of this.viewV2NeedUpdateMap_.keys()) { 682 if (!this.scheduledContainerIds_.has(containerId)) { 683 stateMgmtConsole.debug(` scheduling update for containerId: ${containerId}`); 684 this.scheduledContainerIds_.add(containerId); 685 ViewStackProcessor.scheduleUpdateOnNextVSync(this.onVSyncUpdate.bind(this), containerId); 686 } 687 } 688 stateMgmtConsole.debug(`ObservedV2.updateDirty2Optimized() end`); 689 } 690 // Callback from C++ on VSync 691 public onVSyncUpdate(containerId: number): boolean { 692 stateMgmtConsole.debug(`ObservedV2.flushDirtyViewsOnVSync containerId=${containerId} start`); 693 aceDebugTrace.begin(`ObservedV2.onVSyncUpdate`); 694 let maxFlushTimes = 3; // Refer PipelineContext::FlushDirtyNodeUpdate() 695 // Obtain and unregister the removed elmtIds 696 UINodeRegisterProxy.obtainDeletedElmtIds(); 697 UINodeRegisterProxy.unregisterElmtIdsFromIViews(); 698 699 // Process updates in priority order: computed properties, monitors, UI nodes 700 do { 701 this.updateComputedAndMonitors(); 702 const viewV2Map = this.viewV2NeedUpdateMap_.get(containerId); 703 704 // Clear the ViewV2 map for the current containerId 705 this.viewV2NeedUpdateMap_.delete(containerId); 706 707 if (viewV2Map?.size) { 708 // Update elements, generating new elmtIds in elmtIdsChanged_ for nested updates 709 viewV2Map.forEach((elmtIds, view) => { 710 this.updateUINodesSynchronously(elmtIds, view); 711 }); 712 713 if (this.elmtIdsChanged_.size) { 714 this.groupElementIdsByContainer(); 715 } 716 } else { 717 stateMgmtConsole.error(`No views to update for containerId=${containerId}`); 718 break; // Exit loop early since no updates are possible 719 } 720 } while (this.hasPendingUpdates(containerId) && --maxFlushTimes > 0); 721 722 // Check if more updates are needed 723 const viewV2Map = this.viewV2NeedUpdateMap_.get(containerId); 724 725 if (!viewV2Map || viewV2Map.size === 0) { 726 if (viewV2Map?.size === 0) { 727 this.viewV2NeedUpdateMap_.delete(containerId); 728 } 729 730 ViewStackProcessor.scheduleUpdateOnNextVSync(null, containerId); 731 732 // After all processing, remove from scheduled set 733 this.scheduledContainerIds_.delete(containerId); 734 return false; 735 } 736 aceDebugTrace.end(); 737 stateMgmtConsole.debug(`ObservedV2.onVSyncUpdate there are still views to be updated for containerId=${containerId}`); 738 return true; 739 } 740 741 public hasPendingUpdates(containerId: number): boolean { 742 const viewV2Map = this.viewV2NeedUpdateMap_.get(containerId); 743 let ret = ((viewV2Map && viewV2Map.size > 0) || this.monitorIdsChanged_.size > 0 || this.computedPropIdsChanged_.size > 0); 744 stateMgmtConsole.debug(`hasPendingUpdates() containerId: ${containerId}, viewV2Map size: ${viewV2Map?.size}, ret: ${ret}`); 745 return ret; 746 } 747 748 /** 749 * execute /update in this order 750 * - @Computed variables 751 * - @Monitor functions 752 * - UINode re-render 753 * three nested loops, means: 754 * process @Computed until no more @Computed need update 755 * process @Monitor until no more @Computed and @Monitor 756 * process UINode update until no more @Computed and @Monitor and UINode rerender 757 * 758 * @param updateUISynchronously should be set to true if called during VSYNC only 759 * 760 */ 761 762 public updateDirty2(updateUISynchronously: boolean = false, isReuse: boolean = false): void { 763 aceDebugTrace.begin('updateDirty2'); 764 stateMgmtConsole.debug(`ObservedV2.updateDirty2 updateUISynchronously=${updateUISynchronously} ... `); 765 // obtain and unregister the removed elmtIds 766 UINodeRegisterProxy.obtainDeletedElmtIds(); 767 UINodeRegisterProxy.unregisterElmtIdsFromIViews(); 768 769 // priority order of processing: 770 // 1- update computed properties until no more need computed props update 771 // 2- update monitors until no more monitors and no more computed props 772 // 3- update UINodes until no more monitors, no more computed props, and no more UINodes 773 // FIXME prevent infinite loops 774 do { 775 this.updateComputedAndMonitors(); 776 777 if (this.elmtIdsChanged_.size) { 778 const elmtIds = Array.from(this.elmtIdsChanged_).sort((elmtId1, elmtId2) => elmtId1 - elmtId2); 779 this.elmtIdsChanged_ = new Set<number>(); 780 updateUISynchronously ? isReuse ? this.updateUINodesForReuse(elmtIds) : this.updateUINodesSynchronously(elmtIds) : this.updateUINodes(elmtIds); 781 } 782 } while (this.elmtIdsChanged_.size + this.monitorIdsChanged_.size + this.computedPropIdsChanged_.size > 0); 783 784 aceDebugTrace.end(); 785 stateMgmtConsole.debug(`ObservedV2.updateDirty2 updateUISynchronously=${updateUISynchronously} - DONE `); 786 } 787 788 /** 789 * execute /update in this order 790 * - @Computed variables 791 * - @Monitor functions 792 * - UINode re-render 793 * three nested loops, means: 794 * process @Computed until no more @Computed need update 795 * process @Monitor until no more @Computed and @Monitor 796 */ 797 private updateComputedAndMonitors(): void { 798 do { 799 while (this.computedPropIdsChanged_.size) { 800 // sort the ids and update in ascending order 801 // If a @Computed property depends on other @Computed properties, their 802 // ids will be smaller as they are defined first. 803 const computedProps = Array.from(this.computedPropIdsChanged_).sort((id1, id2) => id1 - id2); 804 this.computedPropIdsChanged_ = new Set<number>(); 805 this.updateDirtyComputedProps(computedProps); 806 } 807 808 if (this.persistenceChanged_.size) { 809 const persistKeys: Array<number> = Array.from(this.persistenceChanged_); 810 this.persistenceChanged_ = new Set<number>(); 811 PersistenceV2Impl.instance().onChangeObserved(persistKeys); 812 } 813 814 if (this.monitorIdsChanged_.size) { 815 const monitors = this.monitorIdsChanged_; 816 this.monitorIdsChanged_ = new Set<number>(); 817 this.updateDirtyMonitors(monitors); 818 } 819 // handle the Monitor id from API configured with asynchronous options 820 while (this.monitorIdsChangedForAddMonitor_.size + this.monitorFuncsToRun_.size > 0) { 821 if (this.monitorIdsChangedForAddMonitor_.size) { 822 stateMgmtConsole.debug(`AddMonitor asynchronous ${this.monitorIdsChangedForAddMonitor_.size}`) 823 const monitorId: Set<number> = this.monitorIdsChangedForAddMonitor_; 824 this.monitorIdsChangedForAddMonitor_ = new Set<number>(); 825 // update the value and dependency for each path and get the MonitorV2 id needs to be execute 826 this.updateDirtyMonitorPath(monitorId); 827 } 828 if (this.monitorFuncsToRun_.size) { 829 const monitorFuncs = this.monitorFuncsToRun_; 830 this.monitorFuncsToRun_ = new Set<number>(); 831 this.runMonitorFunctionsForAddMonitor(monitorFuncs); 832 } 833 } 834 } while (this.monitorIdsChanged_.size + this.persistenceChanged_.size + 835 this.computedPropIdsChanged_.size + this.monitorIdsChangedForAddMonitor_.size + this.monitorFuncsToRun_.size > 0); 836 } 837 838 public updateDirtyComputedProps(computed: Array<number>): void { 839 stateMgmtConsole.debug(`ObservedV2.updateDirtyComputedProps ${computed.length} props: ${JSON.stringify(computed)} ...`); 840 aceDebugTrace.begin(`ObservedV2.updateDirtyComputedProps ${computed.length} @Computed`); 841 computed.forEach((id) => { 842 const comp = this.id2Others_[id]?.deref(); 843 if (comp instanceof ComputedV2) { 844 const target = comp.getTarget(); 845 if (target instanceof ViewV2 && !target.isViewActive()) { 846 // add delayed ComputedIds id 847 target.addDelayedComputedIds(id); 848 } else { 849 comp.fireChange(); 850 } 851 } 852 }); 853 aceDebugTrace.end(); 854 } 855 /** 856 * @function resetMonitorValues 857 * @description This function ensures that @Monitor function are reset and reinitialized 858 * during the reuse cycle: 859 * - Clear and reinitialize monitor IDs and functions to prevent unintended triggers 860 * - Reset dirty states to ensure reusabiltiy 861 */ 862 public resetMonitorValues(): void { 863 stateMgmtConsole.debug(`resetMonitorValues changed monitorIds count: ${this.monitorIdsChanged_.size}`); 864 if (this.monitorIdsChanged_.size) { 865 const monitors = this.monitorIdsChanged_; 866 this.monitorIdsChanged_ = new Set<number>(); 867 this.updateDirtyMonitorsOnReuse(monitors); 868 } 869 } 870 871 public updateDirtyMonitorsOnReuse(monitors: Set<number>): void { 872 let monitor: MonitorV2 | undefined; 873 monitors.forEach((watchId) => { 874 monitor = this.id2Others_[watchId]?.deref(); 875 if (monitor instanceof MonitorV2) { 876 // only update dependency and reset value, no call monitor. 877 monitor.notifyChangeOnReuse(); 878 } 879 }); 880 } 881 882 public updateDirtyMonitors(monitors: Set<number>): void { 883 stateMgmtConsole.debug(`ObservedV2.updateDirtyMonitors: ${monitors.size} @monitor funcs: ${JSON.stringify(Array.from(monitors))} ...`); 884 aceDebugTrace.begin(`ObservedV2.updateDirtyMonitors: ${monitors.size} @monitor`); 885 886 let monitor: MonitorV2 | undefined; 887 let monitorTarget: Object; 888 889 monitors.forEach((watchId) => { 890 monitor = this.id2Others_[watchId]?.deref(); 891 if (monitor instanceof MonitorV2) { 892 monitorTarget = monitor.getTarget(); 893 if (monitorTarget instanceof ViewV2 && !monitorTarget.isViewActive()) { 894 // monitor notifyChange delayed if target is a View that is not active 895 monitorTarget.addDelayedMonitorIds(watchId); 896 } else { 897 monitor.notifyChange(); 898 } 899 } 900 }); 901 aceDebugTrace.end(); 902 } 903 904 public runMonitorFunctionsForAddMonitor(monitors: Set<number>): void { 905 stateMgmtConsole.debug(`ObservedV2.runMonitorFunctionsForAddMonitor: ${monitors.size}. AddMonitor funcs: ${JSON.stringify(Array.from(monitors))} ...`); 906 aceDebugTrace.begin(`ObservedV2.runMonitorFunctionsForAddMonitor: ${monitors.size}`); 907 908 let monitor: MonitorV2 | undefined; 909 910 monitors.forEach((watchId) => { 911 monitor = this.id2Others_[watchId]?.deref(); 912 if (monitor instanceof MonitorV2) { 913 monitor.runMonitorFunction(); 914 } 915 }); 916 aceDebugTrace.end(); 917 } 918 919 920 public updateDirtyMonitorPath(monitors: Set<number>): void { 921 stateMgmtConsole.debug(`ObservedV2.updateDirtyMonitorPath: ${monitors.size} addMonitor funcs: ${JSON.stringify(Array.from(monitors))} ...`); 922 aceDebugTrace.begin(`ObservedV3.updateDirtyMonitorPath: ${monitors.size} addMonitor`); 923 924 let ret: number = 0; 925 monitors.forEach((watchId) => { 926 const monitor = this.id2Others_[watchId]?.deref(); 927 if (monitor instanceof MonitorV2) { 928 const monitorTarget = monitor.getTarget(); 929 if (monitorTarget instanceof ViewV2 && !monitorTarget.isViewActive()) { 930 monitorTarget.addDelayedMonitorIds(watchId) 931 } else { 932 // find the path MonitorValue and record dependency again 933 // get path owning MonitorV2 id 934 ret = monitor.notifyChangeForEachPath(watchId); 935 } 936 } 937 938 // Collect AddMonitor functions that need to be executed later 939 if (ret > 0) { 940 this.monitorFuncsToRun_.add(ret); 941 } 942 }); 943 aceDebugTrace.end(); 944 } 945 946 947 /** 948 * This version of UpdateUINodes does not wait for VSYNC, violates rules 949 * calls UpdateElement, thereby avoids the long and frequent code path from 950 * FlushDirtyNodesUpdate to CustomNode to ViewV2.updateDirtyElements to UpdateElement 951 * Code left here to reproduce benchmark measurements, compare with future optimisation 952 * @param elmtIds 953 * 954 */ 955 private updateUINodesSynchronously(elmtIds: Array<number>, inView?: ViewPU | ViewV2): void { 956 stateMgmtConsole.debug(`ObserveV2.updateUINodesSynchronously: ${elmtIds.length} elmtIds: ${JSON.stringify(elmtIds)} ...`); 957 aceDebugTrace.begin(`ObserveV2.updateUINodesSynchronously: ${elmtIds.length} elmtId`); 958 959 elmtIds.forEach((elmtId) => { 960 let view = inView ?? this.id2cmp_[elmtId]?.deref(); 961 if ((view instanceof ViewV2) || (view instanceof ViewPU)) { 962 if (view.isViewActive()) { 963 // FIXME need to call syncInstanceId before update? 964 view.UpdateElement(elmtId); 965 } else { 966 // schedule delayed update once the view gets active 967 view.scheduleDelayedUpdate(elmtId); 968 } 969 } // if ViewV2 or ViewPU 970 }); 971 aceDebugTrace.end(); 972 } 973 974 private updateUINodesForReuse(elmtIds: Array<number>): void { 975 aceDebugTrace.begin(`ObserveV2.updateUINodesForReuse: ${elmtIds.length} elmtId`); 976 let view: Object; 977 let weak: any; 978 elmtIds.forEach((elmtId) => { 979 if ((weak = this.id2cmp_[elmtId]) && weak && ('deref' in weak) && 980 (view = weak.deref()) && ((view instanceof ViewV2) || (view instanceof ViewPU))) { 981 if (view.isViewActive()) { 982 /* update child element */ this.currentReuseId_ === view.id__() || 983 /* update parameter */ this.currentReuseId_ === elmtId 984 ? view.UpdateElement(elmtId) 985 : view.uiNodeNeedUpdateV2(elmtId); 986 } else { 987 // schedule delayed update once the view gets active 988 view.scheduleDelayedUpdate(elmtId); 989 } 990 } // if ViewV2 or ViewPU 991 }); 992 aceDebugTrace.end(); 993 } 994 995 // This is the code path similar to V2, follows the rule that UI updates on VSYNC. 996 // ViewPU/ViewV2 queues the elmtId that need update, marks the CustomNode dirty in RenderContext 997 // On next VSYNC runs FlushDirtyNodesUpdate to call rerender to call UpdateElement. Much longer code path 998 // much slower 999 private updateUINodes(elmtIds: Array<number>): void { 1000 stateMgmtConsole.debug(`ObserveV2.updateUINodes: ${elmtIds.length} elmtIds need rerender: ${JSON.stringify(elmtIds)} ...`); 1001 aceDebugTrace.begin(`ObserveV2.updateUINodes: ${elmtIds.length} elmtId`); 1002 1003 elmtIds.forEach((elmtId) => { 1004 const view = this.id2cmp_[elmtId]?.deref(); 1005 if ((view instanceof ViewV2) || (view instanceof ViewPU)) { 1006 if (view.isViewActive()) { 1007 view.uiNodeNeedUpdateV2(elmtId); 1008 } else { 1009 // schedule delayed update once the view gets active 1010 view.scheduleDelayedUpdate(elmtId); 1011 } 1012 } 1013 }); 1014 aceDebugTrace.end(); 1015 } 1016 1017 public constructMonitor(owningObject: Object, owningObjectName: string): void { 1018 let watchProp = Symbol.for(MonitorV2.WATCH_PREFIX + owningObjectName); 1019 if (owningObject && (typeof owningObject === 'object') && owningObject[watchProp]) { 1020 Object.entries(owningObject[watchProp]).forEach(([pathString, monitorFunc]) => { 1021 if (monitorFunc && pathString && typeof monitorFunc === 'function') { 1022 const monitor = new MonitorV2(owningObject, pathString, monitorFunc as (m: IMonitor) => void, true); 1023 monitor.InitRun(); 1024 const refs = owningObject[ObserveV2.MONITOR_REFS] ??= {}; 1025 // store a reference inside owningObject 1026 // thereby MonitorV2 will share lifespan as owning @ComponentV2 or @ObservedV2 1027 // remember: id2others only has a WeakRef to MonitorV2 obj 1028 refs[monitorFunc.name] = monitor; 1029 } 1030 // FIXME Else handle error 1031 }); 1032 } // if target[watchProp] 1033 } 1034 1035 public AddMonitorPath(target: object, path: string | string[], monitorFunc: MonitorCallback, options?: MonitorOptions): void { 1036 const funcName = monitorFunc.name; 1037 const refs = target[ObserveV2.ADD_MONITOR_REFS] ??= {}; 1038 let monitor = refs[funcName]; 1039 const pathsUniqueString = Array.isArray(path) ? path.join(' ') : path; 1040 const isSync: boolean = options ? options.isSynchronous : false; 1041 const paths = Array.isArray(path) ? path : [path]; 1042 if (monitor && monitor instanceof MonitorV2) { 1043 if (isSync !== monitor.isSync()) { 1044 stateMgmtConsole.applicationError(`addMonitor failed, current function ${funcName} has already register as ${monitor.isSync()? `sync`: `async`}, cannot change to ${isSync? `sync`: `async`} anymore`); 1045 return; 1046 } 1047 paths.forEach(item => { 1048 monitor.addPath(item); 1049 }); 1050 monitor.InitRun(); 1051 return; 1052 } 1053 1054 monitor = new MonitorV2(target, pathsUniqueString, monitorFunc, false, isSync); 1055 monitor.InitRun(); 1056 // store a reference inside target 1057 // thereby MonitorV2 will share lifespan as owning @ComponentV2 or @ObservedV2 to prevent the MonitorV2 is GC 1058 // remember: id2others only has a WeakRef to MonitorV2 obj 1059 refs[funcName] = monitor; 1060 } 1061 1062 public clearMonitorPath(target: object, path: string | string[], monitorFunc?: MonitorCallback): void { 1063 const refs = target[ObserveV2.ADD_MONITOR_REFS] ??= {}; 1064 const paths = Array.isArray(path) ? path : [path]; 1065 1066 if (monitorFunc) { 1067 const funcName = monitorFunc.name; 1068 let monitor = refs[funcName]; 1069 if (monitor && monitor instanceof MonitorV2) { 1070 paths.forEach(item => { 1071 if (!monitor.removePath(item)) { 1072 stateMgmtConsole.applicationError( 1073 `cannot clear path ${item} for ${funcName} because it was never registered with addMonitor` 1074 ); 1075 } 1076 }); 1077 } else { 1078 const pathsUniqueString = paths.join(' '); 1079 stateMgmtConsole.applicationError( 1080 `clearMonitor failed: cannot clear path(s) ${pathsUniqueString} for ${funcName} because no Monitor instance was found for this function` 1081 ); 1082 } 1083 return; 1084 } 1085 // no monitorFunc passed: try to remove this path for all the monitorV2 instance stored in current target 1086 const monitors = Object.values(refs).filter((m): m is MonitorV2 => m instanceof MonitorV2); 1087 1088 paths.forEach(item => { 1089 let res = false; 1090 monitors.forEach(monitor => { 1091 if (monitor.removePath(item)) { 1092 res = true; 1093 } 1094 }); 1095 if (!res) { 1096 stateMgmtConsole.applicationError( 1097 `cannot clear path ${item} for current target ${target.constructor.name} because no Monitor function for this path was registered` 1098 ); 1099 } 1100 }); 1101 } 1102 1103 public constructComputed(owningObject: Object, owningObjectName: string): void { 1104 const computedProp = Symbol.for(ComputedV2.COMPUTED_PREFIX + owningObjectName); 1105 if (owningObject && (typeof owningObject === 'object') && owningObject[computedProp]) { 1106 Object.entries(owningObject[computedProp]).forEach(([computedPropertyName, computeFunc]) => { 1107 stateMgmtConsole.debug(`constructComputed: in ${owningObject?.constructor?.name} found @Computed ${computedPropertyName}`); 1108 const computed = new ComputedV2(owningObject, computedPropertyName, computeFunc as unknown as () => any); 1109 computed.InitRun(); 1110 const refs = owningObject[ObserveV2.COMPUTED_REFS] ??= {}; 1111 // store a reference inside owningObject 1112 // thereby ComputedV2 will share lifespan as owning @ComponentV2 or @ObservedV2 1113 // remember: id2cmp only has a WeakRef to ComputedV2 obj 1114 const existingComputed = refs[computedPropertyName]; 1115 if (existingComputed && existingComputed instanceof ComputedV2) { 1116 // current computed will be override, and will be GC soon 1117 // to avoid the Computed be triggered anymore, invalidate it 1118 this.clearBinding(existingComputed.getComputedId()); 1119 stateMgmtConsole.warn(`Check compatibility, ${owningObjectName} has @Computed ${computedPropertyName} and create ${owningObject?.constructor?.name} instance`); 1120 } 1121 refs[computedPropertyName] = computed; 1122 }); 1123 } 1124 } 1125 1126 public clearWatch(id: number): void { 1127 if (id < MonitorV2.MIN_WATCH_FROM_API_ID) { 1128 this.clearBinding(id); 1129 return; 1130 } 1131 const monitor: MonitorV2 = this.id2Others_[id]?.deref(); 1132 if (monitor instanceof MonitorV2) { 1133 monitor.getValues().forEach((monitorValueV2: MonitorValueV2<unknown>) => { 1134 this.clearBinding(monitorValueV2.id); 1135 }) 1136 } 1137 this.clearBinding(id); 1138 } 1139 1140 public registerMonitor(monitor: MonitorV2, id: number): void { 1141 const weakRef = WeakRefPool.get(monitor); 1142 // this instance, which maybe MonitorV2/ComputedV2 have been already recorded in id2Others 1143 if (this.id2Others_[id] === weakRef) { 1144 return; 1145 } 1146 this.id2Others_[id] = weakRef; 1147 // register MonitorV2/ComputedV2 instance gc-callback func 1148 WeakRefPool.register(monitor, id, () => { 1149 delete this.id2Others_[id]; 1150 }); 1151 } 1152 1153 1154 public static autoProxyObject(target: Object, key: string | symbol): any { 1155 let val = target[key]; 1156 // Not an object, not a collection, no proxy required 1157 if (!val || typeof (val) !== 'object' || 1158 !(Array.isArray(val) || val instanceof Set || val instanceof Map || val instanceof Date)) { 1159 return val; 1160 } 1161 1162 // Collections are the only type that require proxy observation. If they have already been observed, no further observation is needed. 1163 // Prevents double-proxying: checks if the object is already proxied by either V1 or V2 (to avoid conflicts). 1164 // Prevents V2 proxy creation if the developer uses makeV1Observed and also tries to wrap a V2 proxy with built-in types 1165 // Handle the case where both V1 and V2 proxies exist (if V1 proxy doesn't trigger enableV2Compatibility). 1166 // Currently not implemented to avoid compatibility issues with existing apps that may use both V1 and V2 proxies. 1167 if (!val[ObserveV2.SYMBOL_PROXY_GET_TARGET] && !(ObservedObject.isEnableV2CompatibleInternal(val) || ObservedObject.isMakeV1Observed(val))) { 1168 1169 if (Array.isArray(val)) { 1170 target[key] = new Proxy(val, ObserveV2.arrayProxy); 1171 } else if (val instanceof Set || val instanceof Map) { 1172 target[key] = new Proxy(val, ObserveV2.setMapProxy); 1173 } else { 1174 target[key] = new Proxy(val, ObserveV2.objectProxy); 1175 } 1176 val = target[key]; 1177 } 1178 1179 // If the return value is an Array, Set, Map 1180 // if (this.arr[0] !== undefined, and similar for Set and Map) will not update in response / 1181 // to array length/set or map size changing function without addRef on OB_LENGH 1182 if (!(val instanceof Date)) { 1183 if (ObservedObject.isEnableV2CompatibleInternal(val)) { 1184 ObserveV2.getObserve().addRefV2Compatibility(val, ObserveV2.OB_LENGTH); 1185 } else { 1186 ObserveV2.getObserve().addRef(ObserveV2.IsMakeObserved(val) ? RefInfo.get(UIUtilsImpl.instance().getTarget(val)) : 1187 val, ObserveV2.OB_LENGTH); 1188 } 1189 } 1190 return val; 1191 } 1192 1193 /** 1194 * Helper function to add meta data about decorator to ViewPU or ViewV2 1195 * @param proto prototype object of application class derived from ViewPU or ViewV2 or `@ObservedV2` class 1196 * @param varName decorated variable 1197 * @param deco '@Local', '@Event', etc 1198 * Excludes `@Computed` and `@Monitor` 1199 */ 1200 public static addVariableDecoMeta(proto: Object, varName: string, deco: string): void { 1201 // add decorator meta data 1202 const meta = proto[ObserveV2.V2_DECO_META] ??= {}; 1203 meta[varName] = {}; 1204 meta[varName].deco = deco; 1205 1206 // FIXME 1207 // when splitting ViewPU and ViewV2 1208 // use instanceOf. Until then, this is a workaround. 1209 // any @Local, @Trace, etc V2 event handles this function to return false 1210 Reflect.defineProperty(proto, 'isViewV2', { 1211 get() { return true; }, 1212 enumerable: false 1213 }); 1214 } 1215 1216 /** 1217 * Helper function to add meta data about for `@Computed` and `@Monitor` 1218 * @param proto prototype object of application class derived from ViewPU or ViewV2 or `@ObservedV2` class 1219 * @param varName decorated variable 1220 * @param deco `@Computed` and `@Monitor` 1221 */ 1222 public static addMethodDecoMeta(proto: Object, varName: string, deco: string): void { 1223 // add decorator meta data 1224 const meta = proto[ObserveV2.V2_DECO_METHOD_META] ??= {}; 1225 meta[varName] = {}; 1226 meta[varName].deco = deco; 1227 1228 Reflect.defineProperty(proto, 'isViewV2', { 1229 get() { return true; }, 1230 enumerable: false 1231 }); 1232 } 1233 1234 1235 public static addParamVariableDecoMeta(proto: Object, varName: string, deco?: string, deco2?: string): void { 1236 // add decorator meta data 1237 const meta = proto[ObserveV2.V2_DECO_META] ??= {}; 1238 meta[varName] ??= {}; 1239 if (deco) { 1240 meta[varName].deco = deco; 1241 } 1242 if (deco2) { 1243 meta[varName].deco2 = deco2; 1244 } 1245 1246 // FIXME 1247 // when splitting ViewPU and ViewV2 1248 // use instanceOf. Until then, this is a workaround. 1249 // any @Local, @Trace, etc V2 event handles this function to return false 1250 Reflect.defineProperty(proto, 'isViewV2', { 1251 get() { return true; }, 1252 enumerable: false 1253 } 1254 ); 1255 } 1256 1257 1258 public static usesV2Variables(proto: Object): boolean { 1259 return (proto && typeof proto === 'object' && proto[ObserveV2.V2_DECO_META]); 1260 } 1261 1262 /** 1263 * Get element info according to the elmtId. 1264 * 1265 * @param elmtId element id. 1266 * @param isProfiler need to return ElementType including the id, type and isCustomNode when isProfiler is true. 1267 * The default value is false. 1268 */ 1269 public getElementInfoById(elmtId: number, isProfiler: boolean = false): string | ElementType { 1270 let weak: WeakRef<ViewBuildNodeBase> | undefined = UINodeRegisterProxy.ElementIdToOwningViewPU_.get(elmtId); 1271 let view; 1272 return (weak && (view = weak.deref()) && (view instanceof PUV2ViewBase)) ? view.debugInfoElmtId(elmtId, isProfiler) : `unknown component type[${elmtId}]`; 1273 } 1274 1275 /** 1276 * Get attrName decorator info. 1277 */ 1278 public getDecoratorInfo(target: object, attrName: string): string { 1279 const meta = target[ObserveV2.V2_DECO_META]; 1280 const metaMethod = target[ObserveV2.V2_DECO_METHOD_META]; 1281 const decorator = meta?.[attrName] ?? metaMethod?.[attrName]; 1282 return this.parseDecorator(decorator); 1283 } 1284 1285 public parseDecorator(decorator: any): string { 1286 if (!decorator) { 1287 return ''; 1288 } 1289 if (typeof decorator !== 'object') { 1290 return ''; 1291 } 1292 let decoratorInfo: string = ''; 1293 if ('deco' in decorator) { 1294 decoratorInfo = decorator.deco; 1295 } 1296 if ('aliasName' in decorator) { 1297 decoratorInfo += `(${decorator.aliasName})`; 1298 } 1299 if ('deco2' in decorator) { 1300 decoratorInfo += decorator.deco2; 1301 } 1302 return decoratorInfo; 1303 } 1304 1305 public getComputedInfoById(computedId: number): string { 1306 let weak = this.id2Others_[computedId]; 1307 let computedV2: ComputedV2; 1308 return (weak && (computedV2 = weak.deref()) && (computedV2 instanceof ComputedV2)) ? computedV2.getComputedFuncName() : ''; 1309 } 1310 1311 public getMonitorInfoById(computedId: number): string { 1312 let weak = this.id2Others_[computedId]; 1313 let monitorV2: MonitorV2; 1314 return (weak && (monitorV2 = weak.deref()) && (monitorV2 instanceof MonitorV2)) ? monitorV2.getMonitorFuncName() : ''; 1315 } 1316 1317 public setCurrentReuseId(elmtId: number): void { 1318 this.currentReuseId_ = elmtId; 1319 } 1320} // class ObserveV2 1321 1322const trackInternal = ( 1323 target: any, 1324 propertyKey: string 1325): void => { 1326 if (typeof target === 'function' && !Reflect.has(target, propertyKey)) { 1327 // dynamic track,and it not a static attribute 1328 target = target.prototype; 1329 } 1330 const storeProp = ObserveV2.OB_PREFIX + propertyKey; 1331 target[storeProp] = target[propertyKey]; 1332 Reflect.defineProperty(target, propertyKey, { 1333 get() { 1334 ObserveV2.getObserve().addRef(this, propertyKey); 1335 return ObserveV2.autoProxyObject(this, ObserveV2.OB_PREFIX + propertyKey); 1336 }, 1337 set(val) { 1338 // If the object has not been observed, you can directly assign a value to it. This improves performance. 1339 if (val !== this[storeProp]) { 1340 this[storeProp] = val; 1341 1342 // the bindings <*, target, propertyKey> might not have been recorded yet (!) 1343 // fireChange will run idleTasks to record pending bindings, if any 1344 ObserveV2.getObserve().fireChange(this, propertyKey); 1345 } 1346 }, 1347 enumerable: true 1348 }); 1349 // this marks the proto as having at least one @Trace property inside 1350 // used by IsObservedObjectV2 1351 target[ObserveV2.V2_DECO_META] ??= {}; 1352}; // trackInternal 1353 1354// used to manually mark dirty v2 before animateTo 1355function __updateDirty2Immediately_V2_Change_Observation(): void { 1356 ObserveV2.getObserve().updateDirty2(); 1357}