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