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, IView | 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: IView | 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 public static readonly SYMBOL_REFS = Symbol('__use_refs__'); 57 public static readonly ID_REFS = Symbol('__id_refs__'); 58 public static readonly MONITOR_REFS = Symbol('___monitor_refs_'); 59 public static readonly COMPUTED_REFS = Symbol('___computed_refs_'); 60 61 public static readonly SYMBOL_PROXY_GET_TARGET = Symbol('__proxy_get_target'); 62 63 public static readonly SYMBOL_MAKE_OBSERVED = Symbol('___make_observed__'); 64 65 public static readonly OB_PREFIX = '__ob_'; // OB_PREFIX + attrName => backing store attribute name 66 public static readonly OB_PREFIX_LEN = 5; 67 public static readonly NO_REUSE = -1; // mark no reuse on-going 68 // used by array Handler to create dependency on artificial 'length' 69 // property of array, mark it as changed when array has changed. 70 public static readonly OB_LENGTH = '___obj_length'; 71 72 private static setMapProxy: SetMapProxyHandler = new SetMapProxyHandler(); 73 private static arrayProxy: ArrayProxyHandler = new ArrayProxyHandler(); 74 private static objectProxy: ObjectProxyHandler = new ObjectProxyHandler(); 75 76 // see MonitorV2.observeObjectAccess: bindCmp is the MonitorV2 77 // see modified ViewV2 and ViewPU observeComponentCreation, bindCmp is the ViewV2 or ViewPU 78 79 // bindId: UINode elmtId or watchId, depending on what is being observed 80 private stackOfRenderedComponents_ : StackOfRenderedComponents = new StackOfRenderedComponents(); 81 82 // Map bindId to WeakRef<ViewPU> | MonitorV2 83 private id2cmp_: { number: WeakRef<Object> } = {} as { number: WeakRef<Object> }; 84 85 // Map bindId -> Set of @ObservedV2 class objects 86 // reverse dependency map for quickly removing all dependencies of a bindId 87 private id2targets_: { number: Set<WeakRef<Object>> } = {} as { number: Set<WeakRef<Object>> }; 88 89 // queued up Set of bindId 90 // elmtIds of UINodes need re-render 91 // @monitor functions that need to execute 92 public elmtIdsChanged_: Set<number> = new Set(); 93 private computedPropIdsChanged_: Set<number> = new Set(); 94 private monitorIdsChanged_: Set<number> = new Set(); 95 private persistenceChanged_: Set<number> = new Set(); 96 // avoid recursive execution of updateDirty 97 // by state changes => fireChange while 98 // UINode rerender or @monitor function execution 99 private startDirty_: boolean = false; 100 101 // flag to indicate change observation is disabled 102 private disabled_: boolean = false; 103 104 // flag to indicate ComputedV2 calculation is ongoing 105 private calculatingComputedProp_: boolean = false; 106 107 // use for mark current reuse id, ObserveV2.NO_REUSE(-1) mean no reuse on-going 108 protected currentReuseId_: number = ObserveV2.NO_REUSE; 109 110 private static obsInstance_: ObserveV2; 111 112 public static getObserve(): ObserveV2 { 113 if (!this.obsInstance_) { 114 this.obsInstance_ = new ObserveV2(); 115 } 116 return this.obsInstance_; 117 } 118 119 // return true given value is @ObservedV2 object 120 public static IsObservedObjectV2(value: any): boolean { 121 return (value && typeof (value) === 'object' && value[ObserveV2.V2_DECO_META]); 122 } 123 124 // return true if given value is proxied observed object, either makeObserved or autoProxyObject 125 public static IsProxiedObservedV2(value: any): boolean { 126 return (value && typeof value === 'object' && value[ObserveV2.SYMBOL_PROXY_GET_TARGET]); 127 } 128 129 // return true given value is the return value of makeObserved 130 public static IsMakeObserved(value: any): boolean { 131 return (value && typeof (value) === 'object' && value[ObserveV2.SYMBOL_MAKE_OBSERVED]); 132 } 133 134 public static getCurrentRecordedId(): number { 135 const bound = ObserveV2.getObserve().stackOfRenderedComponents_.top(); 136 return bound ? bound[0] : -1; 137 } 138 139 // At the start of observeComponentCreation or 140 // MonitorV2 observeObjectAccess 141 public startRecordDependencies(cmp: IView | MonitorV2 | ComputedV2 | PersistenceV2Impl | ViewBuildNodeBase, id: number, doClearBinding: boolean = true): void { 142 if (cmp != null) { 143 doClearBinding && this.clearBinding(id); 144 this.stackOfRenderedComponents_.push(id, cmp); 145 } 146 } 147 148 // At the start of observeComponentCreation or 149 // MonitorV2 observeObjectAccess 150 public stopRecordDependencies(): void { 151 const bound = this.stackOfRenderedComponents_.pop(); 152 if (bound === undefined) { 153 stateMgmtConsole.error('stopRecordDependencies finds empty stack. Internal error!'); 154 return; 155 } 156 let targetsSet: Set<WeakRef<Object>>; 157 if ((targetsSet = this.id2targets_[bound[0]]) !== undefined && targetsSet.size) { 158 // only add IView | MonitorV2 | ComputedV2 if at least one dependency was 159 // recorded when rendering this ViewPU/ViewV2/Monitor/ComputedV2 160 // ViewPU is the likely case where no dependecy gets recorded 161 // for others no dependencies are unlikely to happen 162 this.id2cmp_[bound[0]] = new WeakRef<Object>(bound[1]); 163 } 164 } 165 166 // clear any previously created dependency view model object to elmtId 167 // find these view model objects with the reverse map id2targets_ 168 public clearBinding(id: number): void { 169 // multiple weakRefs might point to the same target - here we get Set of unique targets 170 const targetSet = new Set<Object>(); 171 this.id2targets_[id]?.forEach((weak : WeakRef<Object>) => { 172 if (weak.deref() instanceof Object) { 173 targetSet.add(weak.deref()); 174 } 175 }); 176 177 targetSet.forEach((target) => { 178 const idRefs: Object | undefined = target[ObserveV2.ID_REFS]; 179 const symRefs: Object = target[ObserveV2.SYMBOL_REFS]; 180 181 if (idRefs) { 182 idRefs[id]?.forEach(key => symRefs?.[key]?.delete(id)); 183 delete idRefs[id]; 184 } else { 185 for (let key in symRefs) { 186 symRefs[key]?.delete(id); 187 }; 188 } 189 }); 190 191 delete this.id2targets_[id]; 192 delete this.id2cmp_[id]; 193 194 stateMgmtConsole.propertyAccess(`clearBinding (at the end): id2cmp_ length=${Object.keys(this.id2cmp_).length}, entries=${JSON.stringify(Object.keys(this.id2cmp_))} `); 195 stateMgmtConsole.propertyAccess(`... id2targets_ length=${Object.keys(this.id2targets_).length}, entries=${JSON.stringify(Object.keys(this.id2targets_))} `); 196 } 197 198 /** 199 * 200 * this cleanUpId2CmpDeadReferences() 201 * id2cmp is a 'map' object id => WeakRef<Object> where object is ViewV2, ViewPU, MonitorV2 or ComputedV2 202 * This method iterates over the object entries and deleted all those entries whose value can no longer 203 * be deref'ed. 204 * 205 * cleanUpId2TargetsDeadReferences() 206 * is2targets is a 'map' object id => Set<WeakRef<Object>> 207 * the method traverses over the object entries and for each value of type 208 * Set<WeakRef<Object>> removes all those items from the set that can no longer be deref'ed. 209 * 210 * According to JS specifications, it is up to ArlTS runtime GC implementation when to collect unreferences objects. 211 * Parameters such as available memory, ArkTS processing load, number and size of all JS objects for GC collection 212 * can impact the time delay between an object loosing last reference and GC collecting this object. 213 * 214 * WeakRef deref() returns the object until GC has collected it. 215 * The id2cmp and is2targets cleanup herein depends on WeakRef.deref() to return undefined, i.e. it depends on GC 216 * collecting 'cmp' or 'target' objects. Only then the algorithm can remove the entry from id2cmp / from id2target. 217 * It is therefore to be expected behavior that these map objects grow and they a contain a larger number of 218 * MonitorV2, ComputedV2, and/or view model @Observed class objects that are no longer used / referenced by the application. 219 * Only after ArkTS runtime GC has collected them, this function is able to clean up the id2cmp and is2targets. 220 * 221 * This cleanUpDeadReferences() function gets called from UINodeRegisterProxy.uiNodeCleanUpIdleTask() 222 * 223 */ 224 public cleanUpDeadReferences(): void { 225 this.cleanUpId2CmpDeadReferences(); 226 this.cleanUpId2TargetsDeadReferences(); 227 } 228 229 private cleanUpId2CmpDeadReferences(): void { 230 stateMgmtConsole.debug(`cleanUpId2CmpDeadReferences ${JSON.stringify(this.id2cmp_)} `); 231 for (const id in this.id2cmp_) { 232 stateMgmtConsole.debug('cleanUpId2CmpDeadReferences loop'); 233 let weakRef: WeakRef<object> = this.id2cmp_[id]; 234 if (weakRef && typeof weakRef === 'object' && 'deref' in weakRef && weakRef.deref() === undefined) { 235 stateMgmtConsole.debug('cleanUpId2CmpDeadReferences cleanup hit'); 236 delete this.id2cmp_[id]; 237 } 238 } 239 } 240 241 private cleanUpId2TargetsDeadReferences(): void { 242 for (const id in this.id2targets_) { 243 const targetSet: Set<WeakRef<Object>> | undefined = this.id2targets_[id]; 244 if (targetSet && targetSet instanceof Set) { 245 for (let weakTarget of targetSet) { 246 if (weakTarget.deref() === undefined) { 247 stateMgmtConsole.debug('cleanUpId2TargetsDeadReferences cleanup hit'); 248 targetSet.delete(weakTarget); 249 } 250 } // for targetSet 251 } 252 } // for id2targets_ 253 } 254 255 /** 256 * counts number of WeakRef<Object> entries in id2cmp_ 'map' object 257 * @returns total count and count of WeakRefs that can be deref'ed 258 * Methods only for testing 259 */ 260 public get id2CompDeRefSize(): [ totalCount: number, aliveCount: number ] { 261 let totalCount = 0; 262 let aliveCount = 0; 263 let comp: Object; 264 for (const id in this.id2cmp_) { 265 totalCount++; 266 let weakRef: WeakRef<Object> = this.id2cmp_[id]; 267 if (weakRef && 'deref' in weakRef && (comp = weakRef.deref()) && comp instanceof Object) { 268 aliveCount++; 269 } 270 } 271 return [totalCount, aliveCount]; 272 } 273 274 /** counts number of target WeakRef<object> entries in all the Sets inside id2targets 'map' object 275 * @returns total count and those can be dereferenced 276 * Methods only for testing 277 */ 278 public get id2TargetsDerefSize(): [ totalCount: number, aliveCount: number ] { 279 let totalCount = 0; 280 let aliveCount = 0; 281 for (const id in this.id2targets_) { 282 const targetSet: Set<WeakRef<Object>> | undefined = this.id2targets_[id]; 283 if (targetSet && targetSet instanceof Set) { 284 for (let weakTarget of targetSet) { 285 totalCount++; 286 if (weakTarget.deref()) { 287 aliveCount++; 288 } 289 } // for targetSet 290 } 291 } // for id2targets_ 292 return [totalCount, aliveCount]; 293 } 294 295 // add dependency view model object 'target' property 'attrName' 296 // to current this.bindId 297 public addRef(target: object, attrName: string): void { 298 const bound = this.stackOfRenderedComponents_.top(); 299 if (!bound) { 300 return; 301 } 302 if (bound[0] === UINodeRegisterProxy.monitorIllegalV1V2StateAccess) { 303 const error = `${attrName}: ObserveV2.addRef: trying to use V2 state '${attrName}' to init/update child V2 @Component. Application error`; 304 stateMgmtConsole.applicationError(error); 305 throw new TypeError(error); 306 } 307 308 stateMgmtConsole.propertyAccess(`ObserveV2.addRef '${attrName}' for id ${bound[0]}...`); 309 this.addRef4IdInternal(bound[0], target, attrName); 310 } 311 312 // add dependency view model object 'target' property 'attrName' to current this.bindId 313 // this variation of the addRef function is only used to record read access to V1 observed object with enableV2Compatibility enabled 314 // e.g. only from within ObservedObject proxy handler implementations. 315 public addRefV2Compatibility(target: object, attrName: string): void { 316 const bound = this.stackOfRenderedComponents_.top(); 317 if (bound && bound[1]) { 318 if (!(bound[1] instanceof ViewPU)) { 319 if (bound[0] === UINodeRegisterProxy.monitorIllegalV1V2StateAccess) { 320 const error = `${attrName}: ObserveV2.addRefV2Compatibility: trying to use V2 state '${attrName}' to init/update child V2 @Component. Application error`; 321 stateMgmtConsole.applicationError(error); 322 throw new TypeError(error); 323 } 324 stateMgmtConsole.propertyAccess(`ObserveV2.addRefV2Compatibility '${attrName}' for id ${bound[0]}...`); 325 this.addRef4IdInternal(bound[0], target, attrName); 326 } else { 327 // inside ViewPU 328 stateMgmtConsole.propertyAccess(`ObserveV2.addRefV2Compatibility '${attrName}' for id ${bound[0]} -- skip addRef because render/update is inside V1 ViewPU`); 329 } 330 } 331 } 332 333 public addRef4Id(id: number, target: object, attrName: string): void { 334 stateMgmtConsole.propertyAccess(`ObserveV2.addRef4Id '${attrName}' for id ${id} ...`); 335 this.addRef4IdInternal(id, target, attrName); 336 } 337 338 private addRef4IdInternal(id: number, target: object, attrName: string): void { 339 // Map: attribute/symbol -> dependent id 340 const symRefs = target[ObserveV2.SYMBOL_REFS] ??= {}; 341 symRefs[attrName] ??= new Set(); 342 symRefs[attrName].add(id); 343 344 // Map id -> attribute/symbol 345 // optimization for faster clearBinding 346 const idRefs = target[ObserveV2.ID_REFS]; 347 if (idRefs) { 348 idRefs[id] ??= new Set(); 349 idRefs[id].add(attrName); 350 } 351 352 const targetSet = this.id2targets_[id] ??= new Set<WeakRef<Object>>(); 353 targetSet.add(new WeakRef<Object>(target)); 354 } 355 356 /** 357 * 358 * @param target set tracked attribute to new value without notifying the change 359 * !! use with caution !! 360 * @param attrName 361 * @param newValue 362 */ 363 public setUnmonitored<Z>(target: object, attrName: string, newValue: Z): void { 364 const storeProp = ObserveV2.OB_PREFIX + attrName; 365 if (storeProp in target) { 366 // @Track attrName 367 stateMgmtConsole.propertyAccess(`setUnmonitored '${attrName}' - tracked but unchanged. Doing nothing.`); 368 target[storeProp] = newValue; 369 } else { 370 stateMgmtConsole.propertyAccess(`setUnmonitored '${attrName}' - untracked, assigning straight.`); 371 // untracked attrName 372 target[attrName] = newValue; 373 } 374 } 375 376 /** 377 * Execute given task while state change observation is disabled 378 * A state mutation caused by the task will NOT trigger UI rerender 379 * and @monitor function execution. 380 * 381 * !!! Use with Caution !!! 382 * 383 * @param task a function to execute without monitoring state changes 384 * @returns task function return value 385 */ 386 public executeUnobserved<Z>(task: () => Z): Z { 387 stateMgmtConsole.propertyAccess(`executeUnobserved - start`); 388 this.disabled_ = true; 389 let ret: Z; 390 try { 391 ret = task(); 392 } catch (e) { 393 stateMgmtConsole.applicationError(`executeUnobserved - task execution caused error ${e} !`); 394 } 395 this.disabled_ = false; 396 stateMgmtConsole.propertyAccess(`executeUnobserved - done`); 397 return ret; 398 } 399 400 401 402 403 /** 404 * mark view model object 'target' property 'attrName' as changed 405 * notify affected watchIds and elmtIds 406 * 407 * @param propName ObservedV2 or ViewV2 target 408 * @param attrName attrName 409 * @param ignoreOnProfile The data reported to the profiler needs to be the changed state data. 410 * If the fireChange is invoked before the data changed, it needs to be ignored on the profiler. 411 * The default value is false. 412 */ 413 public fireChange(target: object, attrName: string, ignoreOnProfiler: boolean = false): void { 414 // enable to get more fine grained traces 415 // including 2 (!) .end calls. 416 417 if (!target[ObserveV2.SYMBOL_REFS] || this.disabled_) { 418 return; 419 } 420 421 const bound = this.stackOfRenderedComponents_.top(); 422 if (this.calculatingComputedProp_) { 423 const prop = bound ? (bound[1] as ComputedV2).getProp() : 'unknown computed property'; 424 const error = `Usage of ILLEGAL @Computed function detected for ${prop}! The @Computed function MUST NOT change the state of any observed state variable!`; 425 stateMgmtConsole.applicationError(error); 426 throw new Error(error); 427 } 428 429 // enable this trace marker for more fine grained tracing of the update pipeline 430 // note: two (!) end markers need to be enabled 431 let changedIdSet = target[ObserveV2.SYMBOL_REFS][attrName]; 432 if (!changedIdSet || !(changedIdSet instanceof Set)) { 433 return; 434 } 435 436 stateMgmtConsole.propertyAccess(`ObserveV2.fireChange '${attrName}' dependent ids: ${JSON.stringify(Array.from(changedIdSet))} ...`); 437 438 for (const id of changedIdSet) { 439 // Cannot fireChange the object that is being created. 440 if (bound && id === bound[0]) { 441 continue; 442 } 443 444 // if this is the first id to be added to any Set of changed ids, 445 // schedule an 'updateDirty' task 446 // that will run after the current call stack has unwound. 447 // purpose of check for startDirty_ is to avoid going into recursion. This could happen if 448 // exec a re-render or exec a monitor function changes some state -> calls fireChange -> ... 449 if ((this.elmtIdsChanged_.size + this.monitorIdsChanged_.size + this.computedPropIdsChanged_.size === 0) && 450 /* update not already in progress */ !this.startDirty_ && 451 /* no reuse on-going */ this.currentReuseId_ === ObserveV2.NO_REUSE) { 452 Promise.resolve() 453 .then(this.updateDirty.bind(this)) 454 .catch(error => { 455 stateMgmtConsole.applicationError(`Exception occurred during the update process involving @Computed properties, @Monitor functions or UINode re-rendering`, error); 456 _arkUIUncaughtPromiseError(error); 457 }); 458 } 459 460 // add bindId to the correct Set of pending changes. 461 if (id < ComputedV2.MIN_COMPUTED_ID) { 462 this.elmtIdsChanged_.add(id); 463 } else if (id < MonitorV2.MIN_WATCH_ID) { 464 this.computedPropIdsChanged_.add(id); 465 } else if (id < PersistenceV2Impl.MIN_PERSISTENCE_ID) { 466 this.monitorIdsChanged_.add(id); 467 } else { 468 this.persistenceChanged_.add(id); 469 } 470 } // for 471 472 // report the stateVar changed when recording the profiler 473 if (stateMgmtDFX.enableProfiler && !ignoreOnProfiler) { 474 stateMgmtDFX.reportStateInfoToProfilerV2(target, attrName, changedIdSet); 475 } 476 } 477 478 public updateDirty(): void { 479 this.startDirty_ = true; 480 this.updateDirty2(false); 481 this.startDirty_ = false; 482 } 483 484 /** 485 * execute /update in this order 486 * - @Computed variables 487 * - @Monitor functions 488 * - UINode re-render 489 * three nested loops, means: 490 * process @Computed until no more @Computed need update 491 * process @Monitor until no more @Computed and @Monitor 492 * process UINode update until no more @Computed and @Monitor and UINode rerender 493 * 494 * @param updateUISynchronously should be set to true if called during VSYNC only 495 * 496 */ 497 498 public updateDirty2(updateUISynchronously: boolean = false, isReuse: boolean = false): void { 499 aceDebugTrace.begin('updateDirty2'); 500 stateMgmtConsole.debug(`ObservedV2.updateDirty2 updateUISynchronously=${updateUISynchronously} ... `); 501 // obtain and unregister the removed elmtIds 502 UINodeRegisterProxy.obtainDeletedElmtIds(); 503 UINodeRegisterProxy.unregisterElmtIdsFromIViews(); 504 505 // priority order of processing: 506 // 1- update computed properties until no more need computed props update 507 // 2- update monitors until no more monitors and no more computed props 508 // 3- update UINodes until no more monitors, no more computed props, and no more UINodes 509 // FIXME prevent infinite loops 510 do { 511 do { 512 while (this.computedPropIdsChanged_.size) { 513 // sort the ids and update in ascending order 514 // If a @Computed property depends on other @Computed properties, their 515 // ids will be smaller as they are defined first. 516 const computedProps = Array.from(this.computedPropIdsChanged_).sort((id1, id2) => id1 - id2); 517 this.computedPropIdsChanged_ = new Set<number>(); 518 this.updateDirtyComputedProps(computedProps); 519 } 520 521 if (this.persistenceChanged_.size) { 522 const persistKeys: Array<number> = Array.from(this.persistenceChanged_); 523 this.persistenceChanged_ = new Set<number>(); 524 PersistenceV2Impl.instance().onChangeObserved(persistKeys); 525 } 526 527 if (this.monitorIdsChanged_.size) { 528 const monitors = this.monitorIdsChanged_; 529 this.monitorIdsChanged_ = new Set<number>(); 530 this.updateDirtyMonitors(monitors); 531 } 532 } while (this.monitorIdsChanged_.size + this.persistenceChanged_.size + this.computedPropIdsChanged_.size > 0); 533 534 if (this.elmtIdsChanged_.size) { 535 const elmtIds = Array.from(this.elmtIdsChanged_).sort((elmtId1, elmtId2) => elmtId1 - elmtId2); 536 this.elmtIdsChanged_ = new Set<number>(); 537 updateUISynchronously ? isReuse ? this.updateUINodesForReuse(elmtIds) : this.updateUINodesSynchronously(elmtIds) : this.updateUINodes(elmtIds); 538 } 539 } while (this.elmtIdsChanged_.size + this.monitorIdsChanged_.size + this.computedPropIdsChanged_.size > 0); 540 541 stateMgmtConsole.debug(`ObservedV2.updateDirty2 updateUISynchronously=${updateUISynchronously} - DONE `); 542 aceDebugTrace.end(); 543 } 544 545 public updateDirtyComputedProps(computed: Array<number>): void { 546 stateMgmtConsole.debug(`ObservedV2.updateDirtyComputedProps ${computed.length} props: ${JSON.stringify(computed)} ...`); 547 aceDebugTrace.begin(`ObservedV2.updateDirtyComputedProps ${computed.length} @Computed`); 548 computed.forEach((id) => { 549 let comp: ComputedV2 | undefined; 550 let weakComp: WeakRef<ComputedV2 | undefined> = this.id2cmp_[id]; 551 if (weakComp && 'deref' in weakComp && (comp = weakComp.deref()) && comp instanceof ComputedV2) { 552 const target = comp.getTarget(); 553 if (target instanceof ViewV2 && !target.isViewActive()) { 554 // add delayed ComputedIds id 555 target.addDelayedComputedIds(id); 556 } else { 557 comp.fireChange(); 558 } 559 } 560 }); 561 aceDebugTrace.end(); 562 } 563 /** 564 * @function resetMonitorValues 565 * @description This function ensures that @Monitor function are reset and reinitialized 566 * during the reuse cycle: 567 * - Clear and reinitialize monitor IDs and functions to prevent unintended triggers 568 * - Reset dirty states to ensure reusabiltiy 569 */ 570 public resetMonitorValues(): void { 571 stateMgmtConsole.debug(`resetMonitorValues changed monitorIds count: ${this.monitorIdsChanged_.size}`); 572 if (this.monitorIdsChanged_.size) { 573 const monitors = this.monitorIdsChanged_; 574 this.monitorIdsChanged_ = new Set<number>(); 575 this.updateDirtyMonitorsOnReuse(monitors); 576 } 577 } 578 579 public updateDirtyMonitorsOnReuse(monitors: Set<number>): void { 580 let weakMonitor: WeakRef<MonitorV2 | undefined>; 581 let monitor: MonitorV2 | undefined; 582 monitors.forEach((watchId) => { 583 weakMonitor = this.id2cmp_[watchId]; 584 if (weakMonitor && 'deref' in weakMonitor && (monitor = weakMonitor.deref()) && monitor instanceof MonitorV2) { 585 // only update dependency and reset value, no call monitor. 586 monitor.notifyChangeOnReuse(); 587 } 588 }); 589 } 590 591 public updateDirtyMonitors(monitors: Set<number>): void { 592 stateMgmtConsole.debug(`ObservedV3.updateDirtyMonitors: ${Array.from(monitors).length} @monitor funcs: ${JSON.stringify(Array.from(monitors))} ...`); 593 aceDebugTrace.begin(`ObservedV3.updateDirtyMonitors: ${Array.from(monitors).length} @monitor`); 594 let weakMonitor: WeakRef<MonitorV2 | undefined>; 595 let monitor: MonitorV2 | undefined; 596 let monitorTarget: Object; 597 monitors.forEach((watchId) => { 598 weakMonitor = this.id2cmp_[watchId]; 599 if (weakMonitor && 'deref' in weakMonitor && (monitor = weakMonitor.deref()) && monitor instanceof MonitorV2) { 600 if (((monitorTarget = monitor.getTarget()) instanceof ViewV2) && !monitorTarget.isViewActive()) { 601 // monitor notifyChange delayed if target is a View that is not active 602 monitorTarget.addDelayedMonitorIds(watchId); 603 } else { 604 monitor.notifyChange(); 605 } 606 } 607 }); 608 aceDebugTrace.end(); 609 } 610 611 /** 612 * This version of UpdateUINodes does not wait for VSYNC, violates rules 613 * calls UpdateElement, thereby avoids the long and frequent code path from 614 * FlushDirtyNodesUpdate to CustomNode to ViewV2.updateDirtyElements to UpdateElement 615 * Code left here to reproduce benchmark measurements, compare with future optimisation 616 * @param elmtIds 617 * 618 */ 619 private updateUINodesSynchronously(elmtIds: Array<number>): void { 620 stateMgmtConsole.debug(`ObserveV2.updateUINodesSynchronously: ${elmtIds.length} elmtIds: ${JSON.stringify(elmtIds)} ...`); 621 aceDebugTrace.begin(`ObserveV2.updateUINodesSynchronously: ${elmtIds.length} elmtId`); 622 let view: Object; 623 let weak: any; 624 elmtIds.forEach((elmtId) => { 625 if ((weak = this.id2cmp_[elmtId]) && (typeof weak === 'object') && ('deref' in weak) && 626 (view = weak.deref()) && ((view instanceof ViewV2) || (view instanceof ViewPU))) { 627 if (view.isViewActive()) { 628 // FIXME need to call syncInstanceId before update? 629 view.UpdateElement(elmtId); 630 } else { 631 // schedule delayed update once the view gets active 632 view.scheduleDelayedUpdate(elmtId); 633 } 634 } // if ViewV2 or ViewPU 635 }); 636 aceDebugTrace.end(); 637 } 638 639 private updateUINodesForReuse(elmtIds: Array<number>): void { 640 aceDebugTrace.begin(`ObserveV2.updateUINodesForReuse: ${elmtIds.length} elmtId`); 641 let view: Object; 642 let weak: any; 643 elmtIds.forEach((elmtId) => { 644 if ((weak = this.id2cmp_[elmtId]) && weak && ('deref' in weak) && 645 (view = weak.deref()) && ((view instanceof ViewV2) || (view instanceof ViewPU))) { 646 if (view.isViewActive()) { 647 /* update child element */ this.currentReuseId_ === view.id__() || 648 /* update parameter */ this.currentReuseId_ === elmtId 649 ? view.UpdateElement(elmtId) 650 : view.uiNodeNeedUpdateV2(elmtId); 651 } else { 652 // schedule delayed update once the view gets active 653 view.scheduleDelayedUpdate(elmtId); 654 } 655 } // if ViewV2 or ViewPU 656 }); 657 aceDebugTrace.end(); 658 } 659 660 // This is the code path similar to V2, follows the rule that UI updates on VSYNC. 661 // ViewPU/ViewV2 queues the elmtId that need update, marks the CustomNode dirty in RenderContext 662 // On next VSYNC runs FlushDirtyNodesUpdate to call rerender to call UpdateElement. Much longer code path 663 // much slower 664 private updateUINodes(elmtIds: Array<number>): void { 665 stateMgmtConsole.debug(`ObserveV2.updateUINodes: ${elmtIds.length} elmtIds need rerender: ${JSON.stringify(elmtIds)} ...`); 666 aceDebugTrace.begin(`ObserveV2.updateUINodes: ${elmtIds.length} elmtId`); 667 let viewWeak: WeakRef<Object>; 668 let view: Object | undefined; 669 elmtIds.forEach((elmtId) => { 670 viewWeak = this.id2cmp_[elmtId]; 671 if (viewWeak && 'deref' in viewWeak && (view = viewWeak.deref()) && 672 ((view instanceof ViewV2) || (view instanceof ViewPU))) { 673 if (view.isViewActive()) { 674 view.uiNodeNeedUpdateV2(elmtId); 675 } else { 676 // schedule delayed update once the view gets active 677 view.scheduleDelayedUpdate(elmtId); 678 } 679 } 680 }); 681 aceDebugTrace.end(); 682 } 683 684 public constructMonitor(owningObject: Object, owningObjectName: string): void { 685 let watchProp = Symbol.for(MonitorV2.WATCH_PREFIX + owningObjectName); 686 if (owningObject && (typeof owningObject === 'object') && owningObject[watchProp]) { 687 Object.entries(owningObject[watchProp]).forEach(([pathString, monitorFunc]) => { 688 if (monitorFunc && pathString && typeof monitorFunc === 'function') { 689 const monitor = new MonitorV2(owningObject, pathString, monitorFunc as (m: IMonitor) => void); 690 monitor.InitRun(); 691 const refs = owningObject[ObserveV2.MONITOR_REFS] ??= {}; 692 // store a reference inside owningObject 693 // thereby MonitorV2 will share lifespan as owning @ComponentV2 or @ObservedV2 694 // remember: id2cmp only has a WeakRef to MonitorV2 obj 695 refs[monitorFunc.name] = monitor; 696 } 697 // FIXME Else handle error 698 }); 699 } // if target[watchProp] 700 } 701 702 public constructComputed(owningObject: Object, owningObjectName: string): void { 703 const computedProp = Symbol.for(ComputedV2.COMPUTED_PREFIX + owningObjectName); 704 if (owningObject && (typeof owningObject === 'object') && owningObject[computedProp]) { 705 Object.entries(owningObject[computedProp]).forEach(([computedPropertyName, computeFunc]) => { 706 stateMgmtConsole.debug(`constructComputed: in ${owningObject?.constructor?.name} found @Computed ${computedPropertyName}`); 707 const computed = new ComputedV2(owningObject, computedPropertyName, computeFunc as unknown as () => any); 708 computed.InitRun(); 709 const refs = owningObject[ObserveV2.COMPUTED_REFS] ??= {}; 710 // store a reference inside owningObject 711 // thereby ComputedV2 will share lifespan as owning @ComponentV2 or @ObservedV2 712 // remember: id2cmp only has a WeakRef to ComputedV2 obj 713 refs[computedPropertyName] = computed; 714 }); 715 } 716 } 717 718 public clearWatch(id: number): void { 719 this.clearBinding(id); 720 } 721 722 723 724 public static autoProxyObject(target: Object, key: string | symbol): any { 725 let val = target[key]; 726 // Not an object, not a collection, no proxy required 727 if (!val || typeof (val) !== 'object' || 728 !(Array.isArray(val) || val instanceof Set || val instanceof Map || val instanceof Date)) { 729 return val; 730 } 731 732 // Collections are the only type that require proxy observation. If they have already been observed, no further observation is needed. 733 // Prevents double-proxying: checks if the object is already proxied by either V1 or V2 (to avoid conflicts). 734 // Prevents V2 proxy creation if the developer uses makeV1Observed and also tries to wrap a V2 proxy with built-in types 735 // Handle the case where both V1 and V2 proxies exist (if V1 proxy doesn't trigger enableV2Compatibility). 736 // Currently not implemented to avoid compatibility issues with existing apps that may use both V1 and V2 proxies. 737 if (!val[ObserveV2.SYMBOL_PROXY_GET_TARGET] && !(ObservedObject.isEnableV2CompatibleInternal(val) || ObservedObject.isMakeV1Observed(val))) { 738 739 if (Array.isArray(val)) { 740 target[key] = new Proxy(val, ObserveV2.arrayProxy); 741 } else if (val instanceof Set || val instanceof Map) { 742 target[key] = new Proxy(val, ObserveV2.setMapProxy); 743 } else { 744 target[key] = new Proxy(val, ObserveV2.objectProxy); 745 } 746 val = target[key]; 747 } 748 749 // If the return value is an Array, Set, Map 750 // if (this.arr[0] !== undefined, and similar for Set and Map) will not update in response / 751 // to array length/set or map size changing function without addRef on OB_LENGH 752 if (!(val instanceof Date)) { 753 if (ObservedObject.isEnableV2CompatibleInternal(val)) { 754 ObserveV2.getObserve().addRefV2Compatibility(val, ObserveV2.OB_LENGTH); 755 } else { 756 ObserveV2.getObserve().addRef(ObserveV2.IsMakeObserved(val) ? RefInfo.get(UIUtilsImpl.instance().getTarget(val)) : 757 val, ObserveV2.OB_LENGTH); 758 } 759 } 760 return val; 761 } 762 763 /** 764 * Helper function to add meta data about decorator to ViewPU or ViewV2 765 * @param proto prototype object of application class derived from ViewPU or ViewV2 766 * @param varName decorated variable 767 * @param deco '@Local', '@Event', etc 768 */ 769 public static addVariableDecoMeta(proto: Object, varName: string, deco: string): void { 770 // add decorator meta data 771 const meta = proto[ObserveV2.V2_DECO_META] ??= {}; 772 meta[varName] = {}; 773 meta[varName].deco = deco; 774 775 // FIXME 776 // when splitting ViewPU and ViewV2 777 // use instanceOf. Until then, this is a workaround. 778 // any @Local, @Trace, etc V2 event handles this function to return false 779 Reflect.defineProperty(proto, 'isViewV2', { 780 get() { return true; }, 781 enumerable: false 782 } 783 ); 784 } 785 786 787 public static addParamVariableDecoMeta(proto: Object, varName: string, deco?: string, deco2?: string): void { 788 // add decorator meta data 789 const meta = proto[ObserveV2.V2_DECO_META] ??= {}; 790 meta[varName] ??= {}; 791 if (deco) { 792 meta[varName].deco = deco; 793 } 794 if (deco2) { 795 meta[varName].deco2 = deco2; 796 } 797 798 // FIXME 799 // when splitting ViewPU and ViewV2 800 // use instanceOf. Until then, this is a workaround. 801 // any @Local, @Trace, etc V2 event handles this function to return false 802 Reflect.defineProperty(proto, 'isViewV2', { 803 get() { return true; }, 804 enumerable: false 805 } 806 ); 807 } 808 809 810 public static usesV2Variables(proto: Object): boolean { 811 return (proto && typeof proto === 'object' && proto[ObserveV2.V2_DECO_META]); 812 } 813 814 /** 815 * Get element info according to the elmtId. 816 * 817 * @param elmtId element id. 818 * @param isProfiler need to return ElementType including the id, type and isCustomNode when isProfiler is true. 819 * The default value is false. 820 */ 821 public getElementInfoById(elmtId: number, isProfiler: boolean = false): string | ElementType { 822 let weak: WeakRef<ViewBuildNodeBase> | undefined = UINodeRegisterProxy.ElementIdToOwningViewPU_.get(elmtId); 823 let view; 824 return (weak && (view = weak.deref()) && (view instanceof PUV2ViewBase)) ? view.debugInfoElmtId(elmtId, isProfiler) : `unknown component type[${elmtId}]`; 825 } 826 827 /** 828 * Get attrName decorator info. 829 */ 830 public getDecoratorInfo(target: object, attrName: string): string { 831 const meta = target[ObserveV2.V2_DECO_META]; 832 if (!meta) { 833 return ''; 834 } 835 const decorator = meta[attrName]; 836 if (!decorator) { 837 return ''; 838 } 839 let decoratorInfo: string = ''; 840 if ('deco' in decorator) { 841 decoratorInfo = decorator.deco; 842 } 843 if ('aliasName' in decorator) { 844 decoratorInfo += `(${decorator.aliasName})`; 845 } 846 if ('deco2' in decorator) { 847 decoratorInfo += decorator.deco2; 848 } 849 return decoratorInfo; 850 } 851 852 public getComputedInfoById(computedId: number): string { 853 let weak = this.id2cmp_[computedId]; 854 let computedV2: ComputedV2; 855 return (weak && (computedV2 = weak.deref()) && (computedV2 instanceof ComputedV2)) ? computedV2.getComputedFuncName() : ''; 856 } 857 858 public getMonitorInfoById(computedId: number): string { 859 let weak = this.id2cmp_[computedId]; 860 let monitorV2: MonitorV2; 861 return (weak && (monitorV2 = weak.deref()) && (monitorV2 instanceof MonitorV2)) ? monitorV2.getMonitorFuncName() : ''; 862 } 863 864 public setCurrentReuseId(elmtId: number): void { 865 this.currentReuseId_ = elmtId; 866 } 867} // class ObserveV2 868 869 870const trackInternal = ( 871 target: any, 872 propertyKey: string 873): void => { 874 if (typeof target === 'function' && !Reflect.has(target, propertyKey)) { 875 // dynamic track,and it not a static attribute 876 target = target.prototype; 877 } 878 const storeProp = ObserveV2.OB_PREFIX + propertyKey; 879 target[storeProp] = target[propertyKey]; 880 Reflect.defineProperty(target, propertyKey, { 881 get() { 882 ObserveV2.getObserve().addRef(this, propertyKey); 883 return ObserveV2.autoProxyObject(this, ObserveV2.OB_PREFIX + propertyKey); 884 }, 885 set(val) { 886 // If the object has not been observed, you can directly assign a value to it. This improves performance. 887 if (val !== this[storeProp]) { 888 this[storeProp] = val; 889 if (this[ObserveV2.SYMBOL_REFS]) { // This condition can improve performance. 890 ObserveV2.getObserve().fireChange(this, propertyKey); 891 } 892 } 893 }, 894 enumerable: true 895 }); 896 // this marks the proto as having at least one @Trace property inside 897 // used by IsObservedObjectV2 898 target[ObserveV2.V2_DECO_META] ??= {}; 899}; // trackInternal 900