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