1/* 2 * Copyright (c) 2022-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 * * ViewPU - View for Partial Update 16 * 17* all definitions in this file are framework internal 18*/ 19 20type DFXCommand = { what: string, viewId: number, isRecursive: boolean }; 21type RecycleUpdateFunc = (elmtId: number, isFirstRender: boolean, recycleNode: ViewPU) => void; 22type PrebuildFunc = () => void; 23 24/** 25 * A decorator function that sets the static `isReusable_` property to `true` 26 * on the provided class. This decorator is automatically invoked when the generated component 27 * class in the transpiler has the `@Reusable` decorator prefix, as below: 28 * @Reusable 29 * class MyComponent { } 30 */ 31function Reusable<T extends Constructor>(BaseClass: T): T { 32 stateMgmtConsole.debug(`@Reusable ${BaseClass.name}: Redefining isReusable_ as true.`); 33 Reflect.defineProperty(BaseClass.prototype, 'isReusable_', { 34 get: () => { 35 return true; 36 } 37 }); 38 return BaseClass; 39} 40 41class SyncedViewRegistry { 42 public static dirtyNodesList: Set<WeakRef<ViewPU>> = new Set<WeakRef<ViewPU>>(); 43 44 // map viewPU -> weakref<ViewPU> 45 private static wmap_ = new WeakMap(); 46 47 public static addSyncedUpdateDirtyNodes(view: ViewPU): void { 48 let weakRef = this.wmap_.get(view) 49 if (!weakRef) { 50 SyncedViewRegistry.wmap_.set(view, weakRef = new WeakRef(view)); 51 } 52 SyncedViewRegistry.dirtyNodesList.add(weakRef); 53 stateMgmtConsole.debug(`SyncedViewRegistry addSyncedUpdateDirtyNodes ${view.debugInfo__()}, syncDirtySize: ${SyncedViewRegistry.dirtyNodesList.size}`); 54 } 55} 56 57abstract class ViewPU extends PUV2ViewBase 58 implements IViewPropertiesChangeSubscriber, IView { 59 60 // flag for initial rendering or re-render on-going. 61 private isRenderInProgress: boolean = false; 62 63 // flag for initial rendering being done 64 private isInitialRenderDone: boolean = false; 65 66 private runReuse_: boolean = false; 67 68 public paramsGenerator_: () => Object; 69 70 private watchedProps: Map<string, (propName: string) => void> = new Map<string, (propName: string) => void>(); 71 72 private recycleManager_: RecycleManager = undefined; 73 74 public hasBeenRecycled_: boolean = false; 75 76 private preventRecursiveRecycle_: boolean = false; 77 78 private delayRecycleNodeRerender: boolean = false; 79 80 private delayRecycleNodeRerenderDeep: boolean = false; 81 82 // store the current key -> consume, which has the default value 83 public defaultConsume_: Map<string, SynchedPropertyTwoWayPU<any>> = new Map<string, SynchedPropertyTwoWayPU<any>>(); 84 85 // store the current key -> consume, which has reconnect to the provide 86 public reconnectConsume_: Map<string, SynchedPropertyTwoWayPU<any>> = new Map<string, SynchedPropertyTwoWayPU<any>>(); 87 88 // @Provide'd variables by this class and its ancestors 89 protected providedVars_: Map<string, ObservedPropertyAbstractPU<any>> = new Map<string, ObservedPropertyAbstractPU<any>>(); 90 91 // Set of elmtIds that need re-render 92 public dirtyElementIdsNeedsUpdateSynchronously_: Set<number> = new Set<number>(); 93 94 // my LocalStorage instance, shared with ancestor Views. 95 // create a default instance on demand if none is initialized 96 protected localStoragebackStore_: LocalStorage = undefined; 97 98 private ownObservedPropertiesStore__?: Set<ObservedPropertyAbstractPU<any>>; 99 100 private get ownObservedPropertiesStore_() { 101 if (!this.ownObservedPropertiesStore__) { 102 // lazy init 103 this.ownObservedPropertiesStore__ = new Set<ObservedPropertyAbstractPU<any>>(); 104 this.obtainOwnObservedProperties(); 105 } 106 return this.ownObservedPropertiesStore__; 107 } 108 109 protected obtainOwnObservedProperties(): void { 110 let usesStateMgmtVersion = 0; 111 Object.getOwnPropertyNames(this) 112 .filter((propName) => { 113 // do not include backing store, and ObserveV2/MonitorV2/ComputedV2 meta data objects 114 return (propName.startsWith('__') && 115 !propName.startsWith(ObserveV2.OB_PREFIX) && 116 !propName.startsWith(MonitorV2.WATCH_PREFIX) && 117 !propName.startsWith(ComputedV2.COMPUTED_PREFIX)); 118 }) 119 .forEach((propName) => { 120 const stateVar = Reflect.get(this, propName) as Object; 121 if (stateVar && typeof stateVar === 'object' && 'notifyPropertyHasChangedPU' in stateVar) { 122 stateMgmtConsole.debug(`... add state variable ${propName} to ${stateVar}`); 123 this.ownObservedPropertiesStore_.add(stateVar as unknown as ObservedPropertyAbstractPU<any>); 124 usesStateMgmtVersion = 2; 125 } else { 126 stateMgmtConsole.debug(`${this.debugInfo__()} ${propName} application may use an unregular naming style, or stateVar may be Non-Object.`); 127 } 128 }); 129 130 if (this.isViewV2 === true) { 131 if (usesStateMgmtVersion === 2) { 132 const error = `${this.debugInfo__()}: mixed use of stateMgmt V1 and V2 variable decorators. Application error!`; 133 stateMgmtConsole.applicationError(error); 134 throw new Error(error); 135 } 136 } 137 stateMgmtConsole.debug(`${this.debugInfo__()}: uses stateMgmt version ${this.isViewV2 === true ? 3 : 2}`); 138 } 139 140 static _findLocalStorage_ViewPU_Interop?: () => any; 141 142 findLocalStorageInterop?: () => any; 143 144 static _resetFindLocalStorage_ViewPU_Interop() { 145 if (typeof ViewPU._findLocalStorage_ViewPU_Interop === 'function') { 146 ViewPU._findLocalStorage_ViewPU_Interop = undefined; 147 } 148 } 149 150 public get localStorage_(): LocalStorage { 151 if (!this.localStoragebackStore_ && this.getParent()) { 152 stateMgmtConsole.debug(`${this.debugInfo__()}: constructor: get localStorage_ : Using LocalStorage instance of the parent View.`); 153 this.localStoragebackStore_ = this.getParent().localStorage_; 154 } 155 156 // for interop 157 if (InteropConfigureStateMgmt.instance.needsInterop()) { 158 if (!this.localStoragebackStore_) { 159 if (this.findLocalStorageInterop !== undefined && typeof this.findLocalStorageInterop === 'function') { 160 this.localStoragebackStore_ = this.findLocalStorageInterop(); 161 } else if (ViewPU._findLocalStorage_ViewPU_Interop !== undefined && typeof ViewPU._findLocalStorage_ViewPU_Interop === 'function') { 162 this.localStoragebackStore_ = ViewPU._findLocalStorage_ViewPU_Interop(); 163 } 164 } 165 } 166 167 if (!this.localStoragebackStore_) { 168 stateMgmtConsole.info(`${this.debugInfo__()}: constructor: is accessing LocalStorage without being provided an instance. Creating a default instance.`); 169 this.localStoragebackStore_ = new LocalStorage({ /* empty */ }); 170 } 171 return this.localStoragebackStore_; 172 } 173 174 public set localStorage_(instance: LocalStorage) { 175 if (!instance) { 176 // setting to undefined not allowed 177 return; 178 } 179 if (this.localStoragebackStore_) { 180 stateMgmtConsole.applicationError(`${this.debugInfo__()}: constructor: is setting LocalStorage instance twice. Application error.`); 181 } 182 this.localStoragebackStore_ = instance; 183 } 184 185 // FIXME 186 // indicate if this is V1 or a V2 component 187 // V1 by default, changed to V2 by the first V2 decorated variable 188 // when splitting ViewPU and ViewV2 189 // use instanceOf. Until then, this is a workaround. 190 // @Local, @Param, @Trace, etc V2 decorator functions modify isViewV2 to return true 191 // (decorator can modify functions in prototype) 192 // FIXME 193 protected get isViewV2(): boolean { 194 return false; 195 } 196 197 /** 198 * Create a View 199 * 200 * 1. option: top level View, specify 201 * - compilerAssignedUniqueChildId must specify 202 * - parent=undefined 203 * - localStorage must provide if @LocalSTorageLink/Prop variables are used 204 * in this View or descendant Views. 205 * 206 * 2. option: not a top level View 207 * - compilerAssignedUniqueChildId must specify 208 * - parent must specify 209 * - localStorage do not specify, will inherit from parent View. 210 * 211 */ 212 constructor(parent: IView, localStorage: LocalStorage, elmtId: number = UINodeRegisterProxy.notRecordingDependencies, extraInfo: ExtraInfo = undefined) { 213 super(parent, elmtId, extraInfo); 214 // if set use the elmtId also as the ViewPU object's subscribable id. 215 // these matching is requirement for updateChildViewById(elmtId) being able to 216 // find the child ViewPU object by given elmtId 217 //this.id_ = elmtId == UINodeRegisterProxy.notRecordingDependencies ? SubscriberManager.MakeId() : elmtId; 218 219 this.localStoragebackStore_ = undefined; 220 221 stateMgmtConsole.debug(`ViewPU constructor: Creating @Component '${this.constructor.name}' from parent '${parent?.constructor.name}'`); 222 223 ViewBuildNodeBase.arkThemeScopeManager?.onViewPUCreate(this); 224 225 // Disable optimization when V1 is involved. 226 ObserveV2.getObserve().isParentChildOptimizable_ = false; 227 228 if (localStorage) { 229 this.localStorage_ = localStorage; 230 stateMgmtConsole.debug(`${this.debugInfo__()}: constructor: Using LocalStorage instance provided via @Entry or view instance creation.`); 231 } else if (parent instanceof ViewBuildNodeBase) { 232 this.localStorage_ = parent.getShareLocalStorage(); 233 } 234 SubscriberManager.Add(this); 235 stateMgmtConsole.debug(`${this.debugInfo__()}: constructor: done`); 236 } 237 238 // inform the subscribed property 239 // that the View and thereby all properties 240 // are about to be deleted 241 abstract aboutToBeDeleted(): void; 242 243 aboutToReuse(params: Object): void { } 244 245 aboutToRecycle(): void { } 246 247 // super class will call this function from 248 // its aboutToBeDeleted implementation 249 protected aboutToBeDeletedInternal(): void { 250 stateMgmtConsole.debug(`${this.debugInfo__()}: aboutToBeDeletedInternal`); 251 // if this isDeleting_ is true already, it may be set delete status recursively by its parent, so it is not necessary 252 // to set and recursively set its children any more 253 if (!this.isDeleting_) { 254 this.isDeleting_ = true; 255 this.setDeleteStatusRecursively(); 256 } 257 // tell UINodeRegisterProxy that all elmtIds under 258 // this ViewPU should be treated as already unregistered 259 260 // un-register all repeat sub-components 261 this.elmtId2Repeat_.forEach(<I>(repeat: __Repeat<I>) => { 262 repeat.aboutToBeDeleted(); 263 }); 264 265 stateMgmtConsole.debug(`${this.constructor.name}: aboutToBeDeletedInternal `); 266 267 // purge the elmtIds owned by this viewPU from the updateFuncByElmtId and also the state variable dependent elmtIds 268 this.updateFuncByElmtId.forEach((_updateFun: UpdateFuncRecord, elmtId: number) => { 269 this.purgeDeleteElmtId(elmtId); 270 if (ConfigureStateMgmt.instance.needsV2Observe()) { 271 delete ObserveV2.getObserve().id2cmp_[elmtId]; 272 } 273 }); 274 275 if (ConfigureStateMgmt.instance.needsV2Observe()) { 276 delete ObserveV2.getObserve().id2cmp_[this.id_]; 277 } 278 279 if (this.hasRecycleManager()) { 280 this.getRecycleManager().purgeAllCachedRecycleNode(); 281 } 282 283 // un-registration of ElementIDs 284 stateMgmtConsole.debug(`${this.debugInfo__()}: onUnRegElementID`); 285 286 // it will unregister removed elmtIds from all ViewPu, equals purgeDeletedElmtIdsRecursively 287 this.purgeDeletedElmtIds(); 288 289 // unregisters its own id once its children are unregistered above 290 UINodeRegisterProxy.unregisterRemovedElmtsFromViewPUs([this.id__()]); 291 292 stateMgmtConsole.debug(`${this.debugInfo__()}: onUnRegElementID - DONE`); 293 294 // in case this ViewPU is currently frozen 295 PUV2ViewBase.inactiveComponents_.delete(`${this.constructor.name}[${this.id__()}]`); 296 297 this.updateFuncByElmtId.clear(); 298 this.watchedProps.clear(); 299 this.providedVars_.clear(); 300 if (this.ownObservedPropertiesStore__) { 301 this.ownObservedPropertiesStore__.clear(); 302 } 303 if (this.getParent()) { 304 this.getParent().removeChild(this); 305 } 306 ViewBuildNodeBase.arkThemeScopeManager?.onViewPUDelete(this); 307 this.localStoragebackStore_ = undefined; 308 PUV2ViewBase.prebuildFuncQueues.delete(this.id__()); 309 PUV2ViewBase.propertyChangedFuncQueues.delete(this.id__()); 310 // if memory watch register the callback func, then report such information to memory watch 311 // when custom node destroyed 312 if (ArkUIObjectFinalizationRegisterProxy.callbackFunc_) { 313 ArkUIObjectFinalizationRegisterProxy.call(new WeakRef(this), 314 `${this.debugInfo__()} is in the process of destruction` ); 315 } 316 } 317 318 public purgeDeleteElmtId(rmElmtId: number): boolean { 319 stateMgmtConsole.debug(`${this.debugInfo__()} purgeDeleteElmtId (PU) is purging the rmElmtId:${rmElmtId}`); 320 const result = this.updateFuncByElmtId.delete(rmElmtId); 321 if (result) { 322 this.purgeVariableDependenciesOnElmtIdOwnFunc(rmElmtId); 323 // it means rmElmtId has finished all the unregistration from the js side, ElementIdToOwningViewPU_ does not need to keep it 324 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(rmElmtId); 325 } 326 return result; 327 } 328 329 protected purgeVariableDependenciesOnElmtIdOwnFunc(elmtId: number): void { 330 this.ownObservedPropertiesStore_.forEach((stateVar: ObservedPropertyAbstractPU<any>) => { 331 stateVar.purgeDependencyOnElmtId(elmtId); 332 }); 333 } 334 public debugInfoStateVars(): string { 335 let result: string = `|--${this.constructor.name}[${this.id__()}]`; 336 Object.getOwnPropertyNames(this) 337 .filter((varName: string) => varName.startsWith('__') && !varName.startsWith(ObserveV2.OB_PREFIX)) 338 .forEach((varName) => { 339 const prop: any = Reflect.get(this, varName); 340 if (prop && typeof prop === 'object' && 'debugInfoDecorator' in prop) { 341 const observedProp = prop as ObservedPropertyAbstractPU<any>; 342 result += `\n ${observedProp.debugInfoDecorator()} '${observedProp.info()}'[${observedProp.id__()}]`; 343 result += `\n ${observedProp.debugInfoSubscribers()}`; 344 result += `\n ${observedProp.debugInfoSyncPeers()}`; 345 result += `\n ${observedProp.debugInfoDependentElmtIds()}`; 346 result += `\n ${observedProp.debugInfoDependentComponents()}`; 347 } 348 }); 349 return result; 350 } 351 352 /** 353 * Indicate if this @Component is allowed to freeze by calling with freezeState=true 354 * Called with value of the @Component decorator 'freezeWhenInactive' parameter 355 * or depending how UI compiler works also with 'undefined' 356 * @param freezeState only value 'true' will be used, otherwise inherits from parent 357 * if not parent, set to false. 358 */ 359 protected initAllowComponentFreeze(freezeState: boolean | undefined): void { 360 // set to true if freeze parameter set for this @Component to true 361 // otherwise inherit from parent @Component (if it exists). 362 this.isCompFreezeAllowed_ = freezeState || this.isCompFreezeAllowed_; 363 stateMgmtConsole.debug(`${this.debugInfo__()}: @Component freezeWhenInactive state is set to ${this.isCompFreezeAllowed()}`); 364 } 365 366 /** 367 * ArkUI engine will call this function when the corresponding CustomNode's active status change. 368 * ArkUI engine will not recurse children nodes to inform the stateMgmt for the performance reason. 369 * So the stateMgmt needs to recurse the children although the isCompFreezeAllowed is false because the children nodes 370 * may enable the freezeWhenInActive. 371 * @param active true for active, false for inactive 372 */ 373 public setActiveInternal(active: boolean, isReuse = false): void { 374 stateMgmtProfiler.begin('ViewPU.setActive'); 375 if (this.isCompFreezeAllowed()) { 376 this.setActiveCount(active); 377 if (this.isViewActive()) { 378 this.onActiveInternal(); 379 } else { 380 this.onInactiveInternal(); 381 } 382 } 383 // Propagate state to all child View 384 this.propagateToChildren(this.childrenWeakrefMap_, active, isReuse); 385 // Propagate state to all child BuilderNode 386 this.propagateToChildren(this.builderNodeWeakrefMap_, active, isReuse); 387 stateMgmtProfiler.end(); 388 } 389 390 public onActiveInternal(): void { 391 if (!this.isViewActive()) { 392 return; 393 } 394 395 stateMgmtConsole.debug(`${this.debugInfo__()}: onActiveInternal`); 396 this.performDelayedUpdate(); 397 // Remove the active component from the Map for Dfx 398 ViewPU.inactiveComponents_.delete(`${this.constructor.name}[${this.id__()}]`); 399 } 400 401 402 public onInactiveInternal(): void { 403 if (this.isViewActive()) { 404 return; 405 } 406 407 stateMgmtConsole.debug(`${this.debugInfo__()}: onInactiveInternal`); 408 for (const stateLinkProp of this.ownObservedPropertiesStore_) { 409 stateLinkProp.enableDelayedNotification(); 410 } 411 // Add the inactive Components to Map for Dfx listing 412 ViewPU.inactiveComponents_.add(`${this.constructor.name}[${this.id__()}]`); 413 } 414 415 416 // abstract functions to be implemented by application defined class / transpiled code 417 protected abstract purgeVariableDependenciesOnElmtId(removedElmtId: number); 418 protected abstract initialRender(): void; 419 protected abstract rerender(): void; 420 421 public abstract updateRecycleElmtId(oldElmtId: number, newElmtId: number): void; 422 public abstract updateStateVars(params: Object); 423 424 public initialRenderView(): void { 425 stateMgmtProfiler.begin('ViewPU.initialRenderView'); 426 this.onWillApplyThemeInternally(); 427 this.obtainOwnObservedProperties(); 428 this.isRenderInProgress = true; 429 this.initialRender(); 430 this.isRenderInProgress = false; 431 this.isInitialRenderDone = true; 432 stateMgmtProfiler.end(); 433 } 434 435 public UpdateElement(elmtId: number, dirtElmtIdsFromRootNode: Array<number> = new Array<number>()): void { 436 stateMgmtProfiler.begin('ViewPU.UpdateElement'); 437 if (elmtId === this.id__()) { 438 // do not attempt to update itself. 439 // a @Prop can add a dependency of the ViewPU onto itself. Ignore it. 440 stateMgmtProfiler.end(); 441 return; 442 } 443 444 // do not process an Element that has been marked to be deleted 445 const entry: UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId); 446 if (!entry) { 447 stateMgmtProfiler.end(); 448 return; 449 } 450 let updateFunc: UpdateFunc; 451 // if the element is pending, its updateFunc will not be executed during this function call, instead mark its UpdateFuncRecord as changed 452 // when the pending element is retaken and its UpdateFuncRecord is marked changed, then it will be inserted into dirtRetakenElementIds_ 453 if (entry.isPending()) { 454 entry.setIsChanged(true); 455 } else { 456 updateFunc = entry.getUpdateFunc(); 457 } 458 459 if (typeof updateFunc !== 'function') { 460 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: update function of elmtId ${elmtId} not found, internal error!`); 461 } else { 462 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${entry.getComponentName()} elmtId ${elmtId} start ...`); 463 this.isRenderInProgress = true; 464 stateMgmtProfiler.begin('ViewPU.updateFunc'); 465 try { 466 updateFunc(elmtId, /* isFirstRender */ false); 467 } catch (error) { 468 for (const dirtEId of dirtElmtIdsFromRootNode) { 469 stateMgmtConsole.applicationError(`${this.debugInfo__()}: dirty element ${this.updateFuncByElmtId.get(dirtEId)?.getComponentName()} with id ${dirtEId}, isPending: ${this.updateFuncByElmtId.get(dirtEId)?.isPending()}`); 470 } 471 stateMgmtConsole.applicationError(`${this.debugInfo__()}: UpdateElement: re-render of ${entry.getComponentName()} elmtId ${elmtId} has error in update func: ${error.message}`); 472 throw error; 473 } 474 stateMgmtProfiler.end(); 475 stateMgmtProfiler.begin('ViewPU.finishUpdateFunc (native)'); 476 this.finishUpdateFunc(elmtId); 477 stateMgmtProfiler.end(); 478 this.isRenderInProgress = false; 479 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${entry.getComponentName()} elmtId ${elmtId} - DONE`); 480 } 481 stateMgmtProfiler.end(); 482 } 483 484 public delayCompleteRerender(deep: boolean = false): void { 485 this.delayRecycleNodeRerender = true; 486 this.delayRecycleNodeRerenderDeep = deep; 487 } 488 489 public flushDelayCompleteRerender(): void { 490 this.forceCompleteRerender(this.delayRecycleNodeRerenderDeep); 491 this.delayRecycleNodeRerender = false; 492 } 493 494 /** 495 * force a complete rerender / update on specific node by executing update function. 496 * 497 * @param elmtId which node needs to update. 498 * 499 * framework internal functions, apps must not call 500 */ 501 public forceRerenderNode(elmtId: number): void { 502 stateMgmtProfiler.begin('ViewPU/V2.forceRerenderNode'); 503 // see which elmtIds are managed by this View 504 // and clean up all book keeping for them 505 this.purgeDeletedElmtIds(); 506 this.UpdateElement(elmtId); 507 508 // remove elemtId from dirtDescendantElementIds. 509 this.dirtDescendantElementIds_.delete(elmtId); 510 stateMgmtProfiler.end(); 511 } 512 513 // collect elements need to update synchronously and its owning view 514 public collectElementsNeedToUpdateSynchronously(varName: PropertyInfo, dependentElmtIds: Set<number>, isAllowedWatchCallback: boolean): void { 515 stateMgmtConsole.debug(`collectElementsNeedToUpdateSynchronously ${this.debugInfo__()} change ${varName} dependent elements ${dependentElmtIds}`); 516 if (dependentElmtIds.size && !this.isFirstRender()) { 517 for (const elmtId of dependentElmtIds) { 518 if (this.hasRecycleManager()) { 519 this.dirtyElementIdsNeedsUpdateSynchronously_.add(this.recycleManager_.proxyNodeId(elmtId)); 520 } else { 521 this.dirtyElementIdsNeedsUpdateSynchronously_.add(elmtId); 522 } 523 } 524 SyncedViewRegistry.addSyncedUpdateDirtyNodes(this); 525 } 526 if (!isAllowedWatchCallback) { 527 stateMgmtConsole.debug(`${this.debugInfo__()} state var ${varName} does not call @Watch function`); 528 return; 529 } 530 const cb = this.watchedProps.get(varName); 531 if (cb && typeof cb === 'function') { 532 stateMgmtConsole.debug(`${this.debugInfo__()} state var ${varName} calling @Watch function`); 533 cb.call(this, varName); 534 } 535 } 536 537 // implements IMultiPropertiesChangeSubscriber 538 viewPropertyHasChanged(varName: PropertyInfo, dependentElmtIds: Set<number>): void { 539 stateMgmtProfiler.begin('ViewPU.viewPropertyHasChanged'); 540 aceDebugTrace.begin('ViewPU.viewPropertyHasChanged', this.constructor.name, varName, dependentElmtIds.size, this.id__(), this.dirtDescendantElementIds_.size, this.runReuse_); 541 if (this.isRenderInProgress) { 542 stateMgmtConsole.applicationError(`${this.debugInfo__()}: State variable '${varName}' has changed during render! It's illegal to change @Component state while build (initial render or re-render) is on-going. Application error!`); 543 } else if (this.isPrebuilding_) { 544 const propertyChangedFunc: PrebuildFunc = () => { 545 this.viewPropertyHasChanged(varName, dependentElmtIds); 546 }; 547 if (!PUV2ViewBase.propertyChangedFuncQueues.has(this.id__())) { 548 PUV2ViewBase.propertyChangedFuncQueues.set(this.id__(), new Array<PrebuildFunc>()); 549 } 550 PUV2ViewBase.propertyChangedFuncQueues.get(this.id__())?.push(propertyChangedFunc); 551 return; 552 } 553 554 this.syncInstanceId(); 555 556 if (dependentElmtIds.size && !this.isFirstRender()) { 557 if (!this.dirtDescendantElementIds_.size && !this.runReuse_) { 558 // mark ComposedElement dirty when first elmtIds are added 559 // do not need to do this every time 560 this.markNeedUpdate(); 561 } 562 stateMgmtConsole.debug(`${this.debugInfo__()}: viewPropertyHasChanged property: elmtIds that need re-render due to state variable change: ${this.debugInfoElmtIds(Array.from(dependentElmtIds))} .`); 563 for (const elmtId of dependentElmtIds) { 564 if (this.hasRecycleManager()) { 565 this.dirtDescendantElementIds_.add(this.recycleManager_.proxyNodeId(elmtId)); 566 } else { 567 this.dirtDescendantElementIds_.add(elmtId); 568 } 569 } 570 stateMgmtConsole.debug(` ... updated full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`); 571 } else { 572 stateMgmtConsole.debug(`${this.debugInfo__()}: viewPropertyHasChanged: state variable change adds no elmtIds for re-render`); 573 stateMgmtConsole.debug(` ... unchanged full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`); 574 } 575 576 let cb = this.watchedProps.get(varName); 577 if (cb && typeof cb === 'function') { 578 stateMgmtConsole.debug(` ... calling @Watch function`); 579 cb.call(this, varName); 580 } 581 582 this.restoreInstanceId(); 583 aceDebugTrace.end(); 584 stateMgmtProfiler.end(); 585 } 586 587 588 /** 589 * inform that UINode with given elmtId needs rerender 590 * does NOT exec @Watch function. 591 * only used on V2 code path from ObserveV2.fireChange. 592 * 593 * FIXME will still use in the future? 594 */ 595 public uiNodeNeedUpdateV2(elmtId: number): void { 596 if (this.isPrebuilding_) { 597 const propertyChangedFunc: PrebuildFunc = () => { 598 this.uiNodeNeedUpdateV2(elmtId); 599 }; 600 if (!PUV2ViewBase.propertyChangedFuncQueues.has(this.id__())) { 601 PUV2ViewBase.propertyChangedFuncQueues.set(this.id__(), new Array<PrebuildFunc>()); 602 } 603 PUV2ViewBase.propertyChangedFuncQueues.get(this.id__())?.push(propertyChangedFunc); 604 return; 605 } 606 if (this.isFirstRender()) { 607 return; 608 } 609 610 stateMgmtProfiler.begin(`ViewPU.uiNodeNeedUpdate ${this.debugInfoElmtId(elmtId)}`); 611 612 if (!this.dirtDescendantElementIds_.size) { // && !this runReuse_) { 613 // mark ComposedElement dirty when first elmtIds are added 614 // do not need to do this every time 615 this.syncInstanceId(); 616 this.markNeedUpdate(); 617 this.restoreInstanceId(); 618 } 619 if (this.hasRecycleManager()) { 620 this.dirtDescendantElementIds_.add(this.recycleManager_.proxyNodeId(elmtId)); 621 } else { 622 this.dirtDescendantElementIds_.add(elmtId); 623 } 624 stateMgmtConsole.debug(`${this.debugInfo__()}: uiNodeNeedUpdate: updated full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`); 625 626 stateMgmtProfiler.end(); 627 } 628 629 private performDelayedUpdate(): void { 630 if (!this.ownObservedPropertiesStore_.size && !this.elmtIdsDelayedUpdate.size) { 631 return; 632 } 633 stateMgmtProfiler.begin('ViewPU.performDelayedUpdate'); 634 aceDebugTrace.begin('ViewPU.performDelayedUpdate', this.constructor.name); 635 stateMgmtConsole.debug(`${this.debugInfo__()}: performDelayedUpdate start ...`); 636 this.syncInstanceId(); 637 638 for (const stateLinkPropVar of this.ownObservedPropertiesStore_) { 639 const changedElmtIds = stateLinkPropVar.moveElmtIdsForDelayedUpdate(); 640 if (changedElmtIds) { 641 const varName = stateLinkPropVar.info(); 642 if (changedElmtIds.size && !this.isFirstRender()) { 643 for (const elmtId of changedElmtIds) { 644 this.dirtDescendantElementIds_.add(elmtId); 645 } 646 } 647 648 stateMgmtConsole.debug(`${this.debugInfo__()}: performDelayedUpdate: all elmtIds that need re-render [${Array.from(this.dirtDescendantElementIds_).toString()}].`); 649 650 const cb = this.watchedProps.get(varName); 651 if (cb) { 652 stateMgmtConsole.debug(` ... calling @Watch function`); 653 cb.call(this, varName); 654 } 655 } 656 } // for all ownStateLinkProps_ 657 658 for (let elementId of this.elmtIdsDelayedUpdate) { 659 this.dirtDescendantElementIds_.add(elementId); 660 } 661 this.elmtIdsDelayedUpdate.clear(); 662 663 this.restoreInstanceId(); 664 665 if (this.dirtDescendantElementIds_.size) { 666 this.markNeedUpdate(); 667 } 668 aceDebugTrace.end(); 669 stateMgmtProfiler.end(); 670 } 671 672 /** 673 * Function to be called from the constructor of the sub component 674 * to register a @Watch variable 675 * @param propStr name of the variable. Note from @Provide and @Consume this is 676 * the variable name and not the alias! 677 * @param callback application defined member function of sub-class 678 */ 679 protected declareWatch(propStr: string, callback: (propName: string) => void): void { 680 this.watchedProps.set(propStr, callback); 681 } 682 683 /** 684 * This View @Provide's a variable under given name 685 * Call this function from the constructor of the sub class 686 * @param providedPropName either the variable name or the alias defined as 687 * decorator param 688 * @param store the backing store object for this variable (not the get/set variable!) 689 */ 690 protected addProvidedVar<T>(providedPropName: string, store: ObservedPropertyAbstractPU<T>, allowOverride: boolean = false) { 691 if (!allowOverride && this.findProvidePU__(providedPropName)) { 692 throw new ReferenceError(`${this.constructor.name}: duplicate @Provide property with name ${providedPropName}. Property with this name is provided by one of the ancestor Views already. @Provide override not allowed.`); 693 } 694 store.setDecoratorInfo('@Provide'); 695 this.providedVars_.set(providedPropName, store); 696 } 697 698 static _findProvide_ViewPU_Interop?: (providedPropName: string) => any; 699 700 findProvideInterop?: (providedPropName: string) => any; 701 702 /* 703 findProvidePU__ finds @Provided property recursively by traversing ViewPU's towards that of the UI tree root @Component: 704 if 'this' ViewPU has a @Provide('providedPropName') return it, otherwise ask from its parent ViewPU. 705 */ 706 public findProvidePU__(providedPropName: string): ObservedPropertyAbstractPU<any> | undefined { 707 // for interop 708 if (InteropConfigureStateMgmt.instance.needsInterop()) { 709 return this.providedVars_.get(providedPropName) || 710 (this.parent_ && this.parent_.findProvidePU__(providedPropName)) || 711 (this.__parentViewBuildNode__ && this.__parentViewBuildNode__.findProvidePU__(providedPropName)) || 712 (this.findProvideInterop !== undefined && typeof this.findProvideInterop === 'function' ? this.findProvideInterop(providedPropName) : undefined) || 713 (ViewPU._findProvide_ViewPU_Interop !== undefined && typeof ViewPU._findProvide_ViewPU_Interop === 'function' ? 714 ViewPU._findProvide_ViewPU_Interop(providedPropName) : undefined); 715 } 716 return this.providedVars_.get(providedPropName) || 717 (this.parent_ && this.parent_.findProvidePU__(providedPropName)) || 718 (this.__parentViewBuildNode__ && this.__parentViewBuildNode__.findProvidePU__(providedPropName)); 719 } 720 721 static _resetFindProvide_ViewPU_Interop() { 722 if (typeof ViewPU._findProvide_ViewPU_Interop === 'function') { 723 ViewPU._findProvide_ViewPU_Interop = undefined; 724 } 725 } 726 727 /** 728 * Method for the sub-class to call from its constructor for resolving 729 * a @Consume variable and initializing its backing store 730 * with the SyncedPropertyTwoWay<T> object created from the 731 * @Provide variable's backing store. 732 * @param providedPropName the name of the @Provide'd variable. 733 * This is either the @Consume decorator parameter, or variable name. 734 * @param consumeVarName the @Consume variable name (not the 735 * @Consume decorator parameter) 736 * @returns initializing value of the @Consume backing store 737 */ 738 protected initializeConsume<T>(providedPropName: string, 739 consumeVarName: string, defaultValue?: any): ObservedPropertyAbstractPU<T> { 740 let providedVarStore: ObservedPropertyAbstractPU<any> = this.findProvidePU__(providedPropName); 741 // '3' means that developer has initialized the @Consume decorated variable 742 if (!providedVarStore) { 743 if (arguments.length === 3) { 744 providedVarStore = new ObservedPropertyPU(defaultValue, this, consumeVarName); 745 providedVarStore.__setIsFake_ObservedPropertyAbstract_Internal(true); 746 } else { 747 throw new ReferenceError(`${this.debugInfo__()} missing @Provide property with name ${providedPropName} or default value. 748 Fail to resolve @Consume(${providedPropName}).`); 749 } 750 } 751 752 const factory = <T>(source: ObservedPropertyAbstract<T>) => { 753 const result: ObservedPropertyAbstractPU<T> = new SynchedPropertyTwoWayPU<T>(source, this, consumeVarName); 754 result.setDecoratorInfo('@Consume'); 755 stateMgmtConsole.debug(`The @Consume is instance of ${result.constructor.name}`); 756 return result; 757 }; 758 let consumeVal = providedVarStore.createSync(factory) as SynchedPropertyTwoWayPU<T>; 759 if (providedVarStore.__isFake_ObservedPropertyAbstract_Internal()) { 760 this.defaultConsume_.set(providedPropName, consumeVal); 761 } 762 return consumeVal; 763 } 764 765 public reconnectToConsume(): void { 766 this.defaultConsume_.forEach((value: SynchedPropertyObjectTwoWayPU<any>, providedPropName: string) => { 767 let providedVarStore: ObservedPropertyAbstractPU<any> = this.findProvidePU__(providedPropName); 768 if (providedVarStore) { 769 stateMgmtConsole.debug(`${value.debugInfo()} connected to the provide ${providedVarStore.debugInfo()}`); 770 value.resetSource(providedVarStore); 771 // store the consume reconnect to provide 772 this.reconnectConsume_.set(providedPropName, value); 773 value.getDependencies().forEach((id: number) => { 774 this.UpdateElement(id); 775 }) 776 } 777 }) 778 } 779 780 public disconnectedConsume(): void { 781 for (const [key, value] of this.reconnectConsume_) { 782 // try to findProvide again 783 // need to set Parent undefine first 784 const provide = this.findProvidePU__(key); 785 // if provide is undefined, the provide and consume connection is broken 786 // reset consume to default value. 787 if (!provide) { 788 value.resetFakeSource(); 789 value.getDependencies().forEach((id: number) => { 790 this.UpdateElement(id); 791 }) 792 this.reconnectConsume_.delete(key); 793 } 794 } 795 } 796 797 /** 798 * given the elmtId of a child or child of child within this custom component 799 * remember this component needs a partial update 800 * @param elmtId 801 */ 802 public markElemenDirtyById(elmtId: number): void { 803 // TODO ace-ets2bundle, framework, compiled apps need to update together 804 // this function will be removed after a short transition period 805 stateMgmtConsole.applicationError(`${this.debugInfo__()}: markElemenDirtyById no longer supported. 806 Please update your ace-ets2bundle and recompile your application. Application error!`); 807 } 808 809 /** 810 * For each recorded dirty Element in this custom component 811 * run its update function 812 * 813 */ 814 public updateDirtyElements(): void { 815 stateMgmtProfiler.begin('ViewPU.updateDirtyElements'); 816 do { 817 stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render): sorted dirty elmtIds: ${Array.from(this.dirtDescendantElementIds_).sort(ViewPU.compareNumber)}, starting ....`); 818 819 // see which elmtIds are managed by this View 820 // and clean up all book keeping for them 821 this.purgeDeletedElmtIds(); 822 823 // process all elmtIds marked as needing update in ascending order. 824 // ascending order ensures parent nodes will be updated before their children 825 // prior cleanup ensure no already deleted Elements have their update func executed 826 const dirtElmtIdsFromRootNode = Array.from(this.dirtDescendantElementIds_).sort(ViewPU.compareNumber); 827 // if state changed during exec update lambda inside UpdateElement, then the dirty elmtIds will be added 828 // to newly created this.dirtDescendantElementIds_ Set 829 dirtElmtIdsFromRootNode.forEach(elmtId => { 830 if (this.hasRecycleManager()) { 831 this.UpdateElement(this.recycleManager_.proxyNodeId(elmtId), dirtElmtIdsFromRootNode); 832 } else { 833 this.UpdateElement(elmtId, dirtElmtIdsFromRootNode); 834 } 835 this.dirtDescendantElementIds_.delete(elmtId); 836 }); 837 838 if (this.dirtDescendantElementIds_.size) { 839 stateMgmtConsole.applicationError(`${this.debugInfo__()}: New UINode objects added to update queue while re-render! - Likely caused by @Component state change during build phase, not allowed. Application error!`); 840 } 841 842 for (const dirtRetakenElementId of this.dirtRetakenElementIds_) { 843 this.dirtDescendantElementIds_.add(dirtRetakenElementId); 844 } 845 this.dirtRetakenElementIds_.clear(); 846 847 } while (this.dirtDescendantElementIds_.size); 848 stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render) - DONE, dump of ViewPU in next lines`); 849 stateMgmtProfiler.end(); 850 } 851 852 // executed on first render only 853 // kept for backward compatibility with old ace-ets2bundle 854 public observeComponentCreation(compilerAssignedUpdateFunc: UpdateFunc): void { 855 if (PUV2ViewBase.isNeedBuildPrebuildCmd() && PUV2ViewBase.prebuildFuncQueues.has(PUV2ViewBase.prebuildingElmtId_)) { 856 const prebuildFunc: PrebuildFunc = () => { 857 this.observeComponentCreation(compilerAssignedUpdateFunc); 858 }; 859 PUV2ViewBase.prebuildFuncQueues.get(PUV2ViewBase.prebuildingElmtId_)?.push(prebuildFunc); 860 ViewStackProcessor.PushPrebuildCompCmd(); 861 return; 862 } 863 if (this.isDeleting_) { 864 stateMgmtConsole.error(`View ${this.constructor.name} elmtId ${this.id__()} is already in process of destruction, will not execute observeComponentCreation `); 865 return; 866 } 867 const updateFunc = (elmtId: number, isFirstRender: boolean): void => { 868 stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} start ....`); 869 this.currentlyRenderedElmtIdStack_.push(elmtId); 870 compilerAssignedUpdateFunc(elmtId, isFirstRender); 871 this.currentlyRenderedElmtIdStack_.pop(); 872 stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} - DONE ....`); 873 } 874 875 const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent(); 876 // in observeComponentCreation function we do not get info about the component name, in 877 // observeComponentCreation2 we do. 878 this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc }); 879 // add element id -> owning ViewPU 880 UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this)); 881 try { 882 updateFunc(elmtId, /* is first render */ true); 883 } catch (error) { 884 // avoid the incompatible change that move set function before updateFunc. 885 this.updateFuncByElmtId.delete(elmtId); 886 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId); 887 stateMgmtConsole.applicationError(`${this.debugInfo__()} has error in update func: ${(error as Error).message}`); 888 throw error; 889 } 890 } 891 892 public observeComponentCreation2(compilerAssignedUpdateFunc: UpdateFunc, classObject: UIClassObject): void { 893 if (PUV2ViewBase.isNeedBuildPrebuildCmd() && PUV2ViewBase.prebuildFuncQueues.has(PUV2ViewBase.prebuildingElmtId_)) { 894 const prebuildFunc: PrebuildFunc = () => { 895 this.observeComponentCreation2(compilerAssignedUpdateFunc, classObject); 896 }; 897 PUV2ViewBase.prebuildFuncQueues.get(PUV2ViewBase.prebuildingElmtId_)?.push(prebuildFunc); 898 ViewStackProcessor.PushPrebuildCompCmd(); 899 return; 900 } 901 if (this.isDeleting_) { 902 stateMgmtConsole.error(`View ${this.constructor.name} elmtId ${this.id__()} is already in process of destruction, will not execute observeComponentCreation2 `); 903 return; 904 } 905 const _componentName: string = (classObject && ('name' in classObject)) ? Reflect.get(classObject, 'name') as string : 'unspecified UINode'; 906 if (_componentName === '__Recycle__') { 907 return; 908 } 909 const _popFunc: () => void = (classObject && 'pop' in classObject) ? classObject.pop! : (): void => { }; 910 const updateFunc = (elmtId: number, isFirstRender: boolean): void => { 911 this.syncInstanceId(); 912 stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} ${_componentName}[${elmtId}] ${!this.isViewV2 ? '(enable PU state observe) ' : ''} ${ConfigureStateMgmt.instance.needsV2Observe() ? '(enabled V2 state observe) ' : ''} - start ....`); 913 914 ViewBuildNodeBase.arkThemeScopeManager?.onComponentCreateEnter(_componentName, elmtId, isFirstRender, this); 915 916 ViewStackProcessor.StartGetAccessRecordingFor(elmtId); 917 918 if (!this.isViewV2) { 919 // Enable PU state tracking only in PU @Components 920 this.currentlyRenderedElmtIdStack_.push(elmtId); 921 stateMgmtDFX.inRenderingElementId.push(elmtId); 922 } 923 924 // if V2 @Observed/@Track used anywhere in the app (there is no more fine grained criteria), 925 // enable V2 object deep observation 926 // FIXME: A @Component should only use PU or V2 state, but ReactNative dynamic viewer uses both. 927 if (this.isViewV2 || ConfigureStateMgmt.instance.needsV2Observe()) { 928 stateMgmtConsole.debug(`${this.debugInfo__()}: V2 dependency recording is enabled (uses ObserveV2.getObserve().startRecordDependencies, enables addRef)`); 929 // FIXME: like in V2 setting bindId_ in ObserveV2 does not work with 'stacked' 930 // update + initial render calls, like in if and ForEach case, convert to stack as well 931 ObserveV2.getObserve().startRecordDependencies(this, elmtId); 932 } 933 934 compilerAssignedUpdateFunc(elmtId, isFirstRender); 935 if (!isFirstRender) { 936 _popFunc(); 937 } 938 939 let node = this.getNodeById(elmtId); 940 if (node !== undefined) { 941 (node as ArkComponent).cleanStageValue(); 942 } 943 944 if (this.isViewV2 || ConfigureStateMgmt.instance.needsV2Observe()) { 945 ObserveV2.getObserve().stopRecordDependencies(); 946 } 947 if (!this.isViewV2) { 948 this.currentlyRenderedElmtIdStack_.pop(); 949 stateMgmtDFX.inRenderingElementId.pop(); 950 } 951 ViewStackProcessor.StopGetAccessRecording(); 952 953 ViewBuildNodeBase.arkThemeScopeManager?.onComponentCreateExit(elmtId); 954 955 stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} ${_componentName}[${elmtId}] - DONE ....`); 956 this.restoreInstanceId(); 957 }; 958 959 const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent(); 960 // needs to move set before updateFunc. 961 // make sure the key and object value exist since it will add node in attributeModifier during updateFunc. 962 this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc, classObject: classObject }); 963 // add element id -> owning ViewPU 964 UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this)); 965 try { 966 updateFunc(elmtId, /* is first render */ true); 967 } catch (error) { 968 // avoid the incompatible change that move set function before updateFunc. 969 this.updateFuncByElmtId.delete(elmtId); 970 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId); 971 stateMgmtConsole.applicationError(`${this.debugInfo__()} has error in update func: ${(error as Error).message}`); 972 throw error; 973 } 974 stateMgmtConsole.debug(`${this.debugInfo__()} is initial rendering elmtId ${elmtId}, tag: ${_componentName}, and updateFuncByElmtId size :${this.updateFuncByElmtId.size}`); 975 } 976 977 978 getOrCreateRecycleManager(): RecycleManager { 979 if (!this.recycleManager_) { 980 this.recycleManager_ = new RecycleManager; 981 } 982 return this.recycleManager_; 983 } 984 985 getRecycleManager(): RecycleManager { 986 return this.recycleManager_; 987 } 988 989 hasRecycleManager(): boolean { 990 return !(this.recycleManager_ === undefined); 991 } 992 993 initRecycleManager(): void { 994 if (this.recycleManager_) { 995 stateMgmtConsole.error(`${this.debugInfo__()}: init recycleManager multiple times. Internal error.`); 996 return; 997 } 998 this.recycleManager_ = new RecycleManager; 999 } 1000 rebuildUpdateFunc(elmtId: number, compilerAssignedUpdateFunc: UpdateFunc): void { 1001 const updateFunc: UpdateFunc = (elmtId, isFirstRender) => { 1002 this.currentlyRenderedElmtIdStack_.push(elmtId); 1003 compilerAssignedUpdateFunc(elmtId, isFirstRender); 1004 this.currentlyRenderedElmtIdStack_.pop(); 1005 }; 1006 if (this.updateFuncByElmtId.has(elmtId)) { 1007 this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc }); 1008 } 1009 } 1010 1011 /** 1012 * @function observeRecycleComponentCreation 1013 * @description custom node recycle creation 1014 * @param name custom node name 1015 * @param recycleUpdateFunc custom node recycle update which can be converted to a normal update function 1016 * @return void 1017 */ 1018 public observeRecycleComponentCreation(name: string, recycleUpdateFunc: RecycleUpdateFunc): void { 1019 if (PUV2ViewBase.isNeedBuildPrebuildCmd() && PUV2ViewBase.prebuildFuncQueues.has(PUV2ViewBase.prebuildingElmtId_)) { 1020 const prebuildFunc: PrebuildFunc = () => { 1021 this.observeRecycleComponentCreation(name, recycleUpdateFunc); 1022 }; 1023 PUV2ViewBase.prebuildFuncQueues.get(PUV2ViewBase.prebuildingElmtId_)?.push(prebuildFunc); 1024 ViewStackProcessor.PushPrebuildCompCmd(); 1025 return; 1026 } 1027 // convert recycle update func to update func 1028 const compilerAssignedUpdateFunc: UpdateFunc = (element, isFirstRender) => { 1029 recycleUpdateFunc(element, isFirstRender, undefined); 1030 }; 1031 let node: ViewPU; 1032 // if there is no suitable recycle node, run a normal creation function. 1033 if (!this.hasRecycleManager() || !(node = this.getRecycleManager().popRecycleNode(name))) { 1034 stateMgmtConsole.debug(`${this.constructor.name}[${this.id__()}]: cannot init node by recycle, crate new node`); 1035 this.observeComponentCreation(compilerAssignedUpdateFunc); 1036 return; 1037 } 1038 1039 // if there is a suitable recycle node, run a recycle update function. 1040 const newElmtId: number = ViewStackProcessor.AllocateNewElmetIdForNextComponent(); 1041 const oldElmtId: number = node.id__(); 1042 this.recycleManager_.updateNodeId(oldElmtId, newElmtId); 1043 node.hasBeenRecycled_ = false; 1044 this.rebuildUpdateFunc(oldElmtId, compilerAssignedUpdateFunc); 1045 recycleUpdateFunc(oldElmtId, /* is first render */ true, node); 1046 } 1047 1048 // param is used by BuilderNode 1049 aboutToReuseInternal(param?: Object) { 1050 this.runReuse_ = true; 1051 stateMgmtConsole.debug(`ViewPU ${this.debugInfo__()} aboutToReuseInternal`); 1052 stateMgmtTrace.scopedTrace(() => { 1053 if (this.paramsGenerator_ && typeof this.paramsGenerator_ === 'function') { 1054 const params = param ? param : this.paramsGenerator_(); 1055 this.updateStateVars(params); 1056 this.aboutToReuse(params); 1057 } 1058 }, 'aboutToReuse', this.constructor.name); 1059 1060 for (const stateLinkPropVar of this.ownObservedPropertiesStore_) { 1061 const changedElmtIds = stateLinkPropVar.moveElmtIdsForDelayedUpdate(true); 1062 if (changedElmtIds) { 1063 if (changedElmtIds.size && !this.isFirstRender()) { 1064 for (const elmtId of changedElmtIds) { 1065 this.dirtDescendantElementIds_.add(elmtId); 1066 } 1067 } 1068 } 1069 } 1070 if (!this.delayRecycleNodeRerender) { 1071 this.updateDirtyElements(); 1072 } else { 1073 this.flushDelayCompleteRerender(); 1074 } 1075 this.traverseChildDoRecycleOrReuse(PUV2ViewBase.doReuse); 1076 this.runReuse_ = false; 1077 } 1078 1079 stopRecursiveRecycle() { 1080 this.preventRecursiveRecycle_ = true; 1081 } 1082 1083 aboutToRecycleInternal() { 1084 this.runReuse_ = true; 1085 stateMgmtConsole.debug(`ViewPU ${this.debugInfo__()} aboutToRecycleInternal`); 1086 stateMgmtTrace.scopedTrace(() => { 1087 this.aboutToRecycle(); 1088 }, 'aboutToRecycle', this.constructor.name); 1089 if (this.preventRecursiveRecycle_) { 1090 this.preventRecursiveRecycle_ = false; 1091 return; 1092 } 1093 this.traverseChildDoRecycleOrReuse(PUV2ViewBase.doRecycle); 1094 this.runReuse_ = false; 1095 } 1096 1097 // add current JS object to it's parent recycle manager 1098 public recycleSelf(name: string): void { 1099 1100 if (this.getParent() && this.getParent() instanceof ViewPU && !(this.getParent() as ViewPU).isDeleting_) { 1101 const parentPU : ViewPU = this.getParent() as ViewPU; 1102 parentPU.getOrCreateRecycleManager().pushRecycleNode(name, this); 1103 this.hasBeenRecycled_ = true; 1104 } else { 1105 this.resetRecycleCustomNode(); 1106 } 1107 } 1108 1109 public isRecycled() : boolean { 1110 return this.hasBeenRecycled_; 1111 } 1112 1113 public UpdateLazyForEachElements(elmtIds: Array<number>): void { 1114 if (!Array.isArray(elmtIds)) { 1115 return; 1116 } 1117 Array.from(elmtIds).sort(ViewPU.compareNumber).forEach((elmtId: number) => { 1118 const entry: UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId); 1119 const updateFunc: UpdateFunc = entry ? entry.getUpdateFunc() : undefined; 1120 if (typeof updateFunc !== 'function') { 1121 stateMgmtConsole.debug(`${this.debugInfo__()}: update function of elmtId ${elmtId} not found, internal error!`); 1122 } else { 1123 this.isRenderInProgress = true; 1124 updateFunc(elmtId, false); 1125 this.finishUpdateFunc(elmtId); 1126 this.isRenderInProgress = false; 1127 } 1128 }) 1129 } 1130 1131 /** 1132 * CreateStorageLink and CreateStorageLinkPU are used by the implementation of @StorageLink and 1133 * @LocalStotrageLink in full update and partial update solution respectively. 1134 * These are not part of the public AppStorage API , apps should not use. 1135 * @param storagePropName - key in LocalStorage 1136 * @param defaultValue - value to use when creating a new prop in the LocalStotage 1137 * @param owningView - the View/ViewPU owning the @StorageLink/@LocalStorageLink variable 1138 * @param viewVariableName - @StorageLink/@LocalStorageLink variable name 1139 * @returns SynchedPropertySimple/ObjectTwoWay/PU 1140 */ 1141 public createStorageLink<T>(storagePropName: string, defaultValue: T, viewVariableName: string): ObservedPropertyAbstractPU<T> { 1142 const appStorageLink = AppStorage.__createSync<T>(storagePropName, defaultValue, 1143 <T>(source: ObservedPropertyAbstract<T>) => (source === undefined) 1144 ? undefined 1145 : new SynchedPropertyTwoWayPU<T>(source, this, viewVariableName) 1146 ) as ObservedPropertyAbstractPU<T>; 1147 appStorageLink?.setDecoratorInfo('@StorageLink'); 1148 return appStorageLink; 1149 } 1150 1151 public createStorageProp<T>(storagePropName: string, defaultValue: T, viewVariableName: string): ObservedPropertyAbstractPU<T> { 1152 const appStorageProp = AppStorage.__createSync<T>(storagePropName, defaultValue, 1153 <T>(source: ObservedPropertyAbstract<T>) => (source === undefined) 1154 ? undefined 1155 : new SynchedPropertyOneWayPU<T>(source, this, viewVariableName) 1156 ) as ObservedPropertyAbstractPU<T>; 1157 appStorageProp?.setDecoratorInfo('@StorageProp'); 1158 return appStorageProp; 1159 } 1160 1161 public createLocalStorageLink<T>(storagePropName: string, defaultValue: T, 1162 viewVariableName: string): ObservedPropertyAbstractPU<T> { 1163 const localStorageLink = this.localStorage_.__createSync<T>(storagePropName, defaultValue, 1164 <T>(source: ObservedPropertyAbstract<T>) => (source === undefined) 1165 ? undefined 1166 : new SynchedPropertyTwoWayPU<T>(source, this, viewVariableName) 1167 ) as ObservedPropertyAbstractPU<T>; 1168 localStorageLink?.setDecoratorInfo('@LocalStorageLink'); 1169 return localStorageLink; 1170 } 1171 1172 public createLocalStorageProp<T>(storagePropName: string, defaultValue: T, 1173 viewVariableName: string): ObservedPropertyAbstractPU<T> { 1174 const localStorageProp = this.localStorage_.__createSync<T>(storagePropName, defaultValue, 1175 <T>(source: ObservedPropertyAbstract<T>) => (source === undefined) 1176 ? undefined 1177 : new SynchedPropertyObjectOneWayPU<T>(source, this, viewVariableName) 1178 ) as ObservedPropertyAbstractPU<T>; 1179 localStorageProp?.setDecoratorInfo('@LocalStorageProp'); 1180 return localStorageProp; 1181 } 1182 1183 public debugInfoView(recursive: boolean = false): string { 1184 return this.debugInfoViewInternal(recursive); 1185 } 1186 1187 private debugInfoViewInternal(recursive: boolean = false): string { 1188 let retVal: string = `@Component\n${this.constructor.name}[${this.id__()}]`; 1189 retVal += `\n\nView Hierarchy:\n${this.debugInfoViewHierarchy(recursive)}`; 1190 retVal += `\n\nState variables:\n${this.debugInfoStateVars()}`; 1191 retVal += `\n\nRegistered Element IDs:\n${this.debugInfoUpdateFuncByElmtId(recursive)}`; 1192 retVal += `\n\nDirty Registered Element IDs:\n${this.debugInfoDirtDescendantElementIds(recursive)}`; 1193 return retVal; 1194 } 1195 1196 public debugInfoDirtDescendantElementIds(recursive: boolean = false): string { 1197 return this.debugInfoDirtDescendantElementIdsInternal(0, recursive, { total: 0 }); 1198 } 1199 1200 public debugInfoDirtDescendantElementIdsInternal(depth: number = 0, recursive: boolean = false, counter: ProfileRecursionCounter): string { 1201 let retVaL: string = `\n${' '.repeat(depth)}|--${this.constructor.name}[${this.id__()}]: {`; 1202 this.dirtDescendantElementIds_.forEach((value) => { 1203 retVaL += `${value}, `; 1204 }); 1205 counter.total += this.dirtDescendantElementIds_.size; 1206 retVaL += `\n${' '.repeat(depth + 1)}}[${this.dirtDescendantElementIds_.size}]`; 1207 if (recursive) { 1208 this.childrenWeakrefMap_.forEach((value, key, map) => { 1209 retVaL += value.deref()?.debugInfoDirtDescendantElementIdsInternal(depth + 1, recursive, counter); 1210 }) 1211 } 1212 1213 if (recursive && depth == 0) { 1214 retVaL += `\nTotal: ${counter.total}`; 1215 } 1216 return retVaL; 1217 } 1218 1219 /** 1220 * on first render create a new Instance of Repeat 1221 * on re-render connect to existing instance 1222 * @param arr 1223 * @returns 1224 */ 1225 public __mkRepeatAPI: <I>(arr: Array<I>) => RepeatAPI<I> = <I>(arr: Array<I>): RepeatAPI<I> => { 1226 // factory is for future extensions, currently always return the same 1227 const elmtId = this.getCurrentlyRenderedElmtId(); 1228 let repeat = this.elmtId2Repeat_.get(elmtId) as __Repeat<I>; 1229 if (!repeat) { 1230 repeat = new __Repeat<I>(this, arr); 1231 this.elmtId2Repeat_.set(elmtId, repeat); 1232 } else { 1233 repeat.updateArr(arr); 1234 } 1235 1236 return repeat; 1237 } 1238 1239 public reuseOrCreateNewComponent(params: { componentClass: any, getParams: () => Object, 1240 getReuseId?: () => string, extraInfo?: ExtraInfo }): void { 1241 // ViewPU should not have a ReusableV2 Component, throw error! 1242 const error = `@Component cannot have a child @ReusableV2 component !`; 1243 stateMgmtConsole.applicationError(error); 1244 throw new Error(error); 1245 } 1246} // class ViewPU 1247 1248