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