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