1/* 2 * Copyright (c) 2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 17/** 18 * A decorator function that sets the static `isReusable_` property to `true` 19 * on the provided class. This decorator is automatically invoked when the generated component 20 * class in the transpiler has the `@ReusableV2` decorator prefix, as below: 21 * 22 * @ReusableV2 23 * class MyComponent { } 24 */ 25function ReusableV2<T extends Constructor>(BaseClass: T): T { 26 stateMgmtConsole.debug(`@ReusableV2 ${BaseClass.name}: Redefining isReusable_ as true.`); 27 Reflect.defineProperty(BaseClass.prototype, 'isReusable_', { 28 get: () => { 29 return true; 30 } 31 }); 32 return BaseClass; 33} 34 35/** 36 * 37 * This file includes only framework internal classes and functions 38 * non are part of SDK. Do not access from app. 39 * 40 * Implementation of @ComponentV2 is ViewV2 41 * When transpiling @ComponentV2, the transpiler generates a class that extends from ViewV2. 42 * 43 */ 44 45abstract class ViewV2 extends PUV2ViewBase implements IView { 46 47 // Set of elmtIds that need re-render 48 protected dirtDescendantElementIds_: Set<number> = new Set<number>(); 49 50 private monitorIdsDelayedUpdate: Set<number> = new Set(); 51 private computedIdsDelayedUpdate: Set<number> = new Set(); 52 53 private recyclePoolV2_: RecyclePoolV2 | undefined = undefined; 54 55 public hasBeenRecycled_: boolean = false; 56 57 public paramsGenerator_?: () => Object; 58 59 constructor(parent: IView, elmtId: number = UINodeRegisterProxy.notRecordingDependencies, extraInfo: ExtraInfo = undefined) { 60 super(parent, elmtId, extraInfo); 61 this.setIsV2(true); 62 ViewBuildNodeBase.arkThemeScopeManager?.onViewPUCreate(this); 63 stateMgmtConsole.debug(`ViewV2 constructor: Creating @Component '${this.constructor.name}' from parent '${parent?.constructor.name}'`); 64 } 65 66 /** 67 * The `freezeState` parameter determines whether this @ComponentV2 is allowed to freeze, when inactive 68 * Its called with value of the `freezeWhenInactive` parameter from the @ComponentV2 decorator, 69 * or it may be called with `undefined` depending on how the UI compiler works. 70 * 71 * @param freezeState Only the value `true` will be used to set the freeze state, 72 * otherwise it inherits from its parent instance if its freezeState is true 73 */ 74 protected finalizeConstruction(freezeState?: boolean | undefined): void { 75 76 ObserveV2.getObserve().constructComputed(this, this.constructor.name); 77 ObserveV2.getObserve().constructMonitor(this, this.constructor.name); 78 79 // Always use ID_REFS in ViewV2 80 this[ObserveV2.ID_REFS] = {}; 81 82 // set to true if freeze parameter set for this @ComponentV2 to true 83 // otherwise inherit from its parentComponent (if it exists). 84 this.isCompFreezeAllowed_ = freezeState || this.isCompFreezeAllowed_; 85 stateMgmtConsole.debug(`${this.debugInfo__()}: @ComponentV2 freezeWhenInactive state is set to ${this.isCompFreezeAllowed()}`); 86 87 } 88 89 public debugInfo__(): string { 90 return `@ComponentV2 '${this.constructor.name}'[${this.id__()}]`; 91 } 92 93 /** 94 * @function recycleSelf 95 * @description 96 * This callback function is triggered from the native side when the native-side recycle dummy UI node, 97 * acting as the parent for the recycled component, is deleted in the ~RecycleDummyNode() destructor. 98 * It attempts to add the current JS object to the RecyclePool to ensure proper recycling. 99 * 100 * If the parent is invalid or being deleted, the component is reset by invoking the native 101 * `resetRecycleCustomNode` function, which restores the custom node associated with the JSView object: 102 * - If the JSView object has been garbage collected by the engine, the CustomNode is deleted. 103 * - If the JSView object is managed by the RecycleManager, the CustomNode is reused. 104 * 105 * @param {string} reuseId - The ID used for recycling the component. 106 */ 107 public recycleSelf(reuseId: string): void { 108 stateMgmtConsole.debug(`${this.debugInfo__()}: reuseId: ${reuseId}`); 109 110 if (this.getParent() && this.getParent() instanceof ViewV2 && !(this.getParent() as ViewV2).isDeleting_) { 111 const parentV2: ViewV2 = this.getParent() as ViewV2; 112 parentV2.getOrCreateRecyclePool().pushRecycleV2Component(reuseId, this); 113 this.hasBeenRecycled_ = true; 114 } else { 115 // Native function call to restore the custom node for the JSView object 116 // Deletes or reuses the custom node based on GC or RecycleManager 117 this.resetRecycleCustomNode(); 118 } 119 } 120 121 // The resetStateVarsOnReuse function defined in the transpiler will be called. 122 // If it's not defined, it indicates that an older version of the toolchain is being used, 123 // and an error is thrown to notify about the outdated toolchain. 124 public resetStateVarsOnReuse(params: Object): void { 125 throw new Error('Old toolchain detected. Please upgrade to the latest.'); 126 } 127 128 // The aboutToReuse function defined in the application will be called if it exists. 129 // If not, this empty function will be called, which does nothing. 130 aboutToReuse(): void { 131 // Empty function 132 } 133 134 /** 135 * @function aboutToReuseInternal 136 * @description This function is triggered from the function reuseOrCreateNewComponent when the component is 137 * about to be reused from the recycle Pool. 138 * It invokes the `resetStateVarsOnReuse` method (defined in the transpiler) to reinitialize the component's 139 * decorated variables either from its parent or local initialization 140 * It also invokes the `aboutToReuse` function if defined in the application. 141 * Additionally, it recursively traverses all its subcomponents, calling `resetStateVarsOnReuse` 142 * and `aboutToReuse` on each subcomponent to prepare them for reuse. 143 * @param {?Object} initialParams - optional, the first reused component use this params to reset value, or it will not record 144 * dependency of params. 145 */ 146 aboutToReuseInternal(initialParams?: Object): void { 147 stateMgmtConsole.debug(`${this.debugInfo__()}: aboutToReuseInternal`); 148 stateMgmtTrace.scopedTrace(() => { 149 if (this.paramsGenerator_ && typeof this.paramsGenerator_ === 'function') { 150 const params = initialParams ? initialParams : this.paramsGenerator_(); 151 stateMgmtConsole.debug(`${this.debugInfo__()}: resetStateVarsOnReuse params: ${JSON.stringify(params)}`); 152 ObserveV2.getObserve().setCurrentReuseId(this.id__()); 153 // resets the variables to its initial state 154 this.resetStateVarsOnReuse(params); 155 // unfreeze the component on reuse 156 this.unfreezeReusedComponent(); 157 this.aboutToReuse(); 158 } 159 }, 'aboutToReuseInternal', this.constructor.name); 160 ObserveV2.getObserve().updateDirty2(true, true); 161 ObserveV2.getObserve().setCurrentReuseId(ObserveV2.NO_REUSE); 162 this.traverseChildDoRecycleOrReuse(PUV2ViewBase.doReuse); 163 } 164 165 /** 166 * @function aboutToRecycleInternal 167 * @description Callback function invoked from the native side function 'CustomNodeBase::SetRecycleFunction' 168 * when the component is about to be recycled. 169 * It first calls the `aboutToRecycle` function in the application, and performs the necessary actions 170 * defined in the application before recycling. 171 * Then, it freezes the component to avoid performing UI updates when its in recycle pool 172 * Finally recursively traverses all subcomponents, calling `aboutToRecycleInternal` on each subcomponent 173 * that is about to be recycled, preparing them for recycling as well. 174 */ 175 aboutToRecycleInternal(): void { 176 177 stateMgmtConsole.debug(`ViewV2 ${this.debugInfo__()} aboutToRecycleInternal`); 178 179 // Calls the application's aboutToRecycle() method if defined 180 this.aboutToRecycle(); 181 182 // Freeze the component when its in recycle pool 183 this.freezeRecycledComponent(); 184 185 this.traverseChildDoRecycleOrReuse(PUV2ViewBase.doRecycle); 186 } 187 188 // Freezes the component when it is moved to the recycle pool to prevent elementId updates 189 private freezeRecycledComponent(): void { 190 this.activeCount_--; 191 ViewV2.inactiveComponents_.add(`${this.constructor.name}[${this.id__()}]`); 192 } 193 194 /** 195 * @function unfreezeReusedComponent 196 * @description Unfreezes the component when it is removed from the recycle pool for active rendering 197 * Only delayed element update is performed here, as monitors and computed 198 * are reset by resetStateVarsOnReuse() prior to calling this function 199 * 200 * @returns void 201 */ 202 private unfreezeReusedComponent(): void { 203 this.activeCount_++; 204 if (this.elmtIdsDelayedUpdate.size) { 205 this.elmtIdsDelayedUpdate.forEach((element) => { 206 ObserveV2.getObserve().elmtIdsChanged_.add(element); 207 }); 208 } 209 this.elmtIdsDelayedUpdate.clear(); 210 ViewV2.inactiveComponents_.delete(`${this.constructor.name}[${this.id__()}]`); 211 } 212 213 /** 214 * @function getOrCreateRecyclePool 215 * @description Retrieves the existing `RecyclePoolV2` instance or creates a new one 216 * if it does not exist. 217 * 218 * @returns {RecyclePoolV2} - The `RecyclePoolV2` instance for managing recycling. 219 */ 220 getOrCreateRecyclePool(): RecyclePoolV2 { 221 if (!this.recyclePoolV2_) { 222 this.recyclePoolV2_ = new RecyclePoolV2(); 223 } 224 return this.recyclePoolV2_; 225 } 226 227 /** 228 * @function getRecyclePool 229 * @description Retrieves the `RecyclePoolV2` instance if it exists. 230 * @returns {RecyclePoolV2} - The existing `RecyclePoolV2` instance for managing recycling. 231 */ 232 getRecyclePool(): RecyclePoolV2 { 233 return this.recyclePoolV2_; 234 } 235 236 /** 237 * @function hasRecyclePool 238 * @description Checks if a `RecyclePoolV2` instance exists. 239 * The RecyclePoolV2 instance is created when the native side triggers the recycleSelf callback 240 * during the recycling of a component. 241 * @returns {boolean} - `true` if the `RecyclePoolV2` exists, otherwise `false`. 242 */ 243 hasRecyclePool(): boolean { 244 return !(this.recyclePoolV2_ === undefined); 245 } 246 247 /** 248 * @function cleanupRecycledElmtId 249 * @description purges the recycled Element ID in ViewV2 250 * 251 * @returns void 252 */ 253 private cleanupRecycledElmtId(elmtId: number): void { 254 this.updateFuncByElmtId.delete(elmtId); 255 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId); 256 ObserveV2.getObserve().clearBinding(elmtId); 257 } 258 259 protected get isViewV2(): boolean { 260 return true; 261 } 262 263 /** 264 * Virtual function implemented in ViewPU and ViewV2 265 * Unregisters and purges all child elements associated with the specified Element ID in ViewV2. 266 * 267 * @param rmElmtId - The Element ID to be purged and deleted 268 * @returns {boolean} - Returns `true` if the Element ID was successfully deleted, `false` otherwise. 269 */ 270 public purgeDeleteElmtId(rmElmtId: number): boolean { 271 stateMgmtConsole.debug(`${this.debugInfo__()} purgeDeleteElmtId (V2) is purging the rmElmtId:${rmElmtId}`); 272 const result = this.updateFuncByElmtId.delete(rmElmtId); 273 if (result) { 274 const childOpt = this.getChildViewV2ForElmtId(rmElmtId); 275 if (childOpt) { 276 childOpt.setDeleting(); 277 childOpt.setDeleteStatusRecursively(); 278 } 279 280 // it means rmElmtId has finished all the unregistration from the js side, ElementIdToOwningViewPU_ does not need to keep it 281 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(rmElmtId); 282 } 283 284 // Needed only for V2 285 ObserveV2.getObserve().clearBinding(rmElmtId); 286 return result; 287 } 288 289 290 // super class will call this function from 291 // its aboutToBeDeleted implementation 292 protected aboutToBeDeletedInternal(): void { 293 stateMgmtConsole.debug(`${this.debugInfo__()}: aboutToBeDeletedInternal`); 294 // if this isDeleting_ is true already, it may be set delete status recursively by its parent, so it is not necessary 295 // to set and resursively set its children any more 296 if (!this.isDeleting_) { 297 this.isDeleting_ = true; 298 this.setDeleteStatusRecursively(); 299 } 300 // tell UINodeRegisterProxy that all elmtIds under 301 // this ViewV2 should be treated as already unregistered 302 303 stateMgmtConsole.debug(`${this.constructor.name}: aboutToBeDeletedInternal `); 304 305 // purge the elmtIds owned by this ViewV2 from the updateFuncByElmtId and also the state variable dependent elmtIds 306 Array.from(this.updateFuncByElmtId.keys()).forEach((elmtId: number) => { 307 // FIXME split View: enable delete this purgeDeleteElmtId(elmtId); 308 }); 309 310 // unregistration of ElementIDs 311 stateMgmtConsole.debug(`${this.debugInfo__()}: onUnRegElementID`); 312 313 // Clears all cached components from the Recycle pool and resets the customNode on the native side 314 if (this.hasRecyclePool()) { 315 this.getRecyclePool().purgeAllCachedRecycleElmtIds(); 316 } 317 318 // it will unregister removed elementids from all the ViewV2, equals purgeDeletedElmtIdsRecursively 319 this.purgeDeletedElmtIds(); 320 321 // unregisters its own id once its children are unregistered above 322 UINodeRegisterProxy.unregisterRemovedElmtsFromViewPUs([this.id__()]); 323 324 stateMgmtConsole.debug(`${this.debugInfo__()}: onUnRegElementID - DONE`); 325 326 PUV2ViewBase.inactiveComponents_.delete(`${this.constructor.name}[${this.id__()}]`); 327 328 MonitorV2.clearWatchesFromTarget(this); 329 ComputedV2.clearComputedFromTarget(this); 330 331 this.updateFuncByElmtId.clear(); 332 if (this.parent_) { 333 this.parent_.removeChild(this); 334 } 335 ViewBuildNodeBase.arkThemeScopeManager?.onViewPUDelete(this); 336 } 337 338 public initialRenderView(): void { 339 stateMgmtProfiler.begin(`ViewV2: initialRenderView`); 340 if (this.isReusable_ === true) { 341 const isReusableAllowed = this.allowReusableV2Descendant(); 342 if (!isReusableAllowed) { 343 const error = `Using @ReusableV2 component inside Repeat.template or other invalid parent component is not allowed!`; 344 stateMgmtConsole.applicationError(error); 345 throw new Error(error); 346 } 347 } 348 this.onWillApplyThemeInternally(); 349 this.initialRender(); 350 stateMgmtProfiler.end(); 351 } 352 353 /** 354 * @function resetMonitorsOnReuse 355 * @description 356 * Called from the transpiler's `resetStateVarsOnReuse` method when the component is about to be reused. 357 * Ensures that @Monitor functions are reset and reinitialized during the reuse cycle 358 */ 359 public resetMonitorsOnReuse(): void { 360 // Clear the monitorIds set for delayed updates, if any 361 this.monitorIdsDelayedUpdate.clear(); 362 ObserveV2.getObserve().resetMonitorValues(); 363 } 364 365 // Resets the computed value when the reused component variables are reinitialized 366 // through the resetStateVarsOnReuse process 367 public resetComputed(name: string): void { 368 // Clear the computedIds set for delayed updates, if any 369 this.computedIdsDelayedUpdate.clear(); 370 371 const refs = this[ObserveV2.COMPUTED_REFS]; 372 refs[name].resetComputed(name); 373 } 374 375 // Resets the consumer value when the component is reinitialized on reuse 376 public resetConsumer<T>(varName: string, consumerVal: T): void { 377 let providerInfo = ProviderConsumerUtilV2.findProvider(this, varName); 378 if (!providerInfo) { 379 ProviderConsumerUtilV2.defineConsumerWithoutProvider(this, varName, consumerVal); 380 ObserveV2.getObserve().fireChange(this, varName); 381 } 382 stateMgmtConsole.debug(`resetConsumer value: ${consumerVal} for ${varName}`); 383 } 384 385 public observeComponentCreation2(compilerAssignedUpdateFunc: UpdateFunc, classObject: { prototype: Object, pop?: () => void }): void { 386 if (this.isNeedBuildPrebuildCmd() && PUV2ViewBase.prebuildFuncQueues.has(PUV2ViewBase.prebuildingElmtId_)) { 387 const prebuildFunc: PrebuildFunc = () => { 388 this.observeComponentCreation2(compilerAssignedUpdateFunc, classObject); 389 }; 390 PUV2ViewBase.prebuildFuncQueues.get(PUV2ViewBase.prebuildingElmtId_)?.push(prebuildFunc); 391 ViewStackProcessor.PushPrebuildCompCmd(); 392 return; 393 } 394 if (this.isDeleting_) { 395 stateMgmtConsole.error(`@ComponentV2 ${this.constructor.name} elmtId ${this.id__()} is already in process of destruction, will not execute observeComponentCreation2 `); 396 return; 397 } 398 const _componentName: string = (classObject && ('name' in classObject)) ? Reflect.get(classObject, 'name') as string : 'unspecified UINode'; 399 const _popFunc: () => void = (classObject && 'pop' in classObject) ? classObject.pop! : (): void => { }; 400 const updateFunc = (elmtId: number, isFirstRender: boolean): void => { 401 this.syncInstanceId(); 402 stateMgmtConsole.debug(`@ComponentV2 ${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} ${_componentName}[${elmtId}] - start ....`); 403 ViewBuildNodeBase.arkThemeScopeManager?.onComponentCreateEnter(_componentName, elmtId, isFirstRender, this); 404 ViewStackProcessor.StartGetAccessRecordingFor(elmtId); 405 ObserveV2.getObserve().startRecordDependencies(this, elmtId); 406 407 compilerAssignedUpdateFunc(elmtId, isFirstRender); 408 if (!isFirstRender) { 409 _popFunc(); 410 } 411 412 let node = this.getNodeById(elmtId); 413 if (node !== undefined) { 414 (node as ArkComponent).cleanStageValue(); 415 } 416 417 ObserveV2.getObserve().stopRecordDependencies(); 418 ViewStackProcessor.StopGetAccessRecording(); 419 ViewBuildNodeBase.arkThemeScopeManager?.onComponentCreateExit(elmtId); 420 stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} ${_componentName}[${elmtId}] - DONE ....`); 421 this.restoreInstanceId(); 422 }; 423 424 const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent(); 425 // needs to move set before updateFunc. 426 // make sure the key and object value exist since it will add node in attributeModifier during updateFunc. 427 this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc, classObject: classObject }); 428 // add element id -> owning ViewV2 429 UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this)); 430 try { 431 updateFunc(elmtId, /* is first render */ true); 432 } catch (error) { 433 // avoid the incompatible change that move set function before updateFunc. 434 this.updateFuncByElmtId.delete(elmtId); 435 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId); 436 stateMgmtConsole.applicationError(`${this.debugInfo__()} has error in update func: ${(error as Error).message}`); 437 throw error; 438 } 439 stateMgmtConsole.debug(`${this.debugInfo__()} is initial rendering elmtId ${elmtId}, tag: ${_componentName}, and updateFuncByElmtId size :${this.updateFuncByElmtId.size}`); 440 } 441 442 /** 443 * 444 * @param paramVariableName 445 * @Param @Once paramVariableName 446 * @Param is read only, therefore, init from parent needs to be done without 447 * causing property setter() to be called 448 * @param newValue 449 */ 450 protected initParam<Z>(paramVariableName: string, newValue: Z): void { 451 VariableUtilV2.initParam<Z>(this, paramVariableName, newValue); 452 } 453 /** 454 * 455 * @param paramVariableName 456 * @Param @Once paramVariableName 457 * @Param is read only, therefore, update from parent needs to be done without 458 * causing property setter() to be called 459 * @Param @Once reject any update 460 * @param newValue 461 */ 462 protected updateParam<Z>(paramVariableName: string, newValue: Z): void { 463 VariableUtilV2.updateParam<Z>(this, paramVariableName, newValue); 464 } 465 466 protected resetParam<Z>(paramVariableName: string, newValue: Z): void { 467 VariableUtilV2.resetParam<Z>(this, paramVariableName, newValue); 468 } 469 470 /** 471 * inform that UINode with given elmtId needs rerender 472 * does NOT exec @Watch function. 473 * only used on V2 code path from ObserveV2.fireChange. 474 * 475 * FIXME will still use in the future? 476 */ 477 public uiNodeNeedUpdateV2(elmtId: number): void { 478 if (this.isPrebuilding_) { 479 const propertyChangedFunc: PrebuildFunc = () => { 480 this.uiNodeNeedUpdateV2(elmtId); 481 }; 482 if (!PUV2ViewBase.propertyChangedFuncQueues.has(this.id__())) { 483 PUV2ViewBase.propertyChangedFuncQueues.set(this.id__(), new Array<PrebuildFunc>()); 484 } 485 PUV2ViewBase.propertyChangedFuncQueues.get(this.id__())?.push(propertyChangedFunc); 486 return; 487 } 488 if (this.isFirstRender()) { 489 return; 490 } 491 492 stateMgmtProfiler.begin(`ViewV2.uiNodeNeedUpdate ${this.debugInfoElmtId(elmtId)}`); 493 494 if (!this.isViewActive()) { 495 this.scheduleDelayedUpdate(elmtId); 496 return; 497 } 498 499 if (!this.dirtDescendantElementIds_.size) { // && !this runReuse_) { 500 // mark ComposedElement dirty when first elmtIds are added 501 // do not need to do this every time 502 this.syncInstanceId(); 503 this.markNeedUpdate(); 504 this.restoreInstanceId(); 505 } 506 this.dirtDescendantElementIds_.add(elmtId); 507 stateMgmtConsole.debug(`${this.debugInfo__()}: uiNodeNeedUpdate: updated full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`); 508 stateMgmtProfiler.end(); 509 } 510 511 512 /** 513 * For each recorded dirty Element in this custom component 514 * run its update function 515 * 516 */ 517 public updateDirtyElements(): void { 518 stateMgmtProfiler.begin('ViewV2.updateDirtyElements'); 519 do { 520 stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render): sorted dirty elmtIds: ${Array.from(this.dirtDescendantElementIds_).sort(ViewV2.compareNumber)}, starting ....`); 521 522 // see which elmtIds are managed by this View 523 // and clean up all book keeping for them 524 this.purgeDeletedElmtIds(); 525 526 // process all elmtIds marked as needing update in ascending order. 527 // ascending order ensures parent nodes will be updated before their children 528 // prior cleanup ensure no already deleted Elements have their update func executed 529 const dirtElmtIdsFromRootNode = Array.from(this.dirtDescendantElementIds_).sort(ViewV2.compareNumber); 530 // if state changed during exec update lambda inside UpdateElement, then the dirty elmtIds will be added 531 // to newly created this.dirtDescendantElementIds_ Set 532 dirtElmtIdsFromRootNode.forEach(elmtId => { 533 this.UpdateElement(elmtId); 534 this.dirtDescendantElementIds_.delete(elmtId); 535 }); 536 537 if (this.dirtDescendantElementIds_.size) { 538 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!`); 539 } 540 541 for (const dirtRetakenElementId of this.dirtRetakenElementIds_) { 542 this.dirtDescendantElementIds_.add(dirtRetakenElementId); 543 } 544 this.dirtRetakenElementIds_.clear(); 545 } while (this.dirtDescendantElementIds_.size); 546 stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render) - DONE`); 547 stateMgmtProfiler.end(); 548 } 549 550 551 public UpdateElement(elmtId: number): void { 552 553 if(this.isDeleting_) { 554 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement(${elmtId}) (V2) returns with NO UPDATE, this @ComponentV2 is under deletion!`); 555 return; 556 } 557 558 stateMgmtProfiler.begin('ViewV2.UpdateElement'); 559 if (elmtId === this.id__()) { 560 // do not attempt to update itself 561 stateMgmtProfiler.end(); 562 return; 563 } 564 // do not process an Element that has been marked to be deleted 565 const entry: UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId); 566 if (!entry) { 567 stateMgmtProfiler.end(); 568 return; 569 } 570 let updateFunc: UpdateFunc; 571 // if the element is pending, its updateFunc will not be executed during this function call, instead mark its UpdateFuncRecord as changed 572 // when the pending element is retaken and its UpdateFuncRecord is marked changed, then it will be inserted into dirtRetakenElementIds_ 573 if (entry.isPending()) { 574 entry.setIsChanged(true); 575 } else { 576 updateFunc = entry.getUpdateFunc(); 577 } 578 579 if (typeof updateFunc !== 'function') { 580 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: update function of elmtId ${elmtId} not found, internal error!`); 581 } else { 582 const componentName = entry.getComponentName(); 583 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${componentName} elmtId ${elmtId} start ...`); 584 stateMgmtProfiler.begin('ViewV2.updateFunc'); 585 try { 586 updateFunc(elmtId, /* isFirstRender */ false); 587 } catch (e) { 588 stateMgmtConsole.applicationError(`Exception caught in update function of ${componentName} for elmtId ${elmtId}`, e.toString()); 589 throw e; 590 } finally { 591 stateMgmtProfiler.end(); 592 } 593 stateMgmtProfiler.begin('ViewV2.finishUpdateFunc (native)'); 594 this.finishUpdateFunc(elmtId); 595 stateMgmtProfiler.end(); 596 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${componentName} elmtId ${elmtId} - DONE`); 597 } 598 stateMgmtProfiler.end(); 599 } 600 601 /** 602 * Retrieve child by given id 603 * @param id 604 * @returns child if child with this id exists and it is instance of ViewV2 605 */ 606 public getViewV2ChildById(id: number): ViewV2 | undefined { 607 const childWeakRef = this.childrenWeakrefMap_.get(id); 608 const child = childWeakRef ? childWeakRef.deref() : undefined; 609 return (child && child instanceof ViewV2) ? child : undefined; 610 } 611 612 // WatchIds that needs to be fired later gets added to monitorIdsDelayedUpdate 613 // monitor fireChange will be triggered for all these watchIds once this view gets active 614 public addDelayedMonitorIds(watchId: number): void { 615 stateMgmtConsole.debug(`${this.debugInfo__()} addDelayedMonitorIds called for watchId: ${watchId}`); 616 this.monitorIdsDelayedUpdate.add(watchId); 617 } 618 619 public addDelayedComputedIds(watchId: number) { 620 stateMgmtConsole.debug(`${this.debugInfo__()} addDelayedComputedIds called for watchId: ${watchId}`); 621 this.computedIdsDelayedUpdate.add(watchId); 622 } 623 // If the component has `hasComponentFreezeEnabled` set to true and is marked as @ReusableV2, 624 // skip the delayed update, as freeze and delayed updates are handled in `aboutToRecycleInternal` 625 // and `aboutToReuseInternal` for @ReusableV2 components. 626 public setActiveInternal(active: boolean, isReuse: boolean = false): void { 627 stateMgmtProfiler.begin('ViewV2.setActive'); 628 stateMgmtConsole.debug(`${this.debugInfo__()}: isCompFreezeAllowed : ${this.isCompFreezeAllowed()}`); 629 if (this.isCompFreezeAllowed() && !isReuse) { 630 stateMgmtConsole.debug(`${this.debugInfo__()}: ViewV2.setActive ${active ? ' inActive -> active' : 'active -> inActive'}`); 631 this.setActiveCount(active); 632 if (this.isViewActive()) { 633 this.performDelayedUpdate(); 634 ViewV2.inactiveComponents_.delete(`${this.constructor.name}[${this.id__()}]`); 635 } else { 636 ViewV2.inactiveComponents_.add(`${this.constructor.name}[${this.id__()}]`); 637 } 638 } 639 for (const child of this.childrenWeakrefMap_.values()) { 640 const childView: IView | undefined = child.deref(); 641 if (childView) { 642 childView.setActiveInternal(active, isReuse); 643 } 644 } 645 stateMgmtProfiler.end(); 646 } 647 648 private performDelayedUpdate(): void { 649 stateMgmtProfiler.begin('ViewV2: performDelayedUpdate'); 650 if(this.computedIdsDelayedUpdate.size) { 651 // exec computed functions 652 ObserveV2.getObserve().updateDirtyComputedProps([...this.computedIdsDelayedUpdate]); 653 } 654 if(this.monitorIdsDelayedUpdate.size) { 655 // exec monitor functions 656 ObserveV2.getObserve().updateDirtyMonitors(this.monitorIdsDelayedUpdate); 657 } 658 if(this.elmtIdsDelayedUpdate.size) { 659 // update re-render of updated element ids once the view gets active 660 if(this.dirtDescendantElementIds_.size === 0) { 661 this.dirtDescendantElementIds_ = new Set(this.elmtIdsDelayedUpdate); 662 } 663 else { 664 this.elmtIdsDelayedUpdate.forEach((element) => { 665 this.dirtDescendantElementIds_.add(element); 666 }); 667 } 668 } 669 this.markNeedUpdate(); 670 this.elmtIdsDelayedUpdate.clear(); 671 this.monitorIdsDelayedUpdate.clear(); 672 this.computedIdsDelayedUpdate.clear(); 673 stateMgmtProfiler.end(); 674 } 675 676 /* 677 findProvidePU finds @Provided property recursively by traversing ViewPU's towards that of the UI tree root @Component: 678 if 'this' ViewPU has a @Provide('providedPropName') return it, otherwise ask from its parent ViewPU. 679 function needed for mixed @Component and @ComponentV2 parent child hierarchies. 680 */ 681 public findProvidePU(providedPropName: string): ObservedPropertyAbstractPU<any> | undefined { 682 return this.getParent()?.findProvidePU(providedPropName); 683 } 684 685 get localStorage_(): LocalStorage { 686 // FIXME check this also works for root @ComponentV2 687 return (this.getParent()) ? this.getParent().localStorage_ : new LocalStorage({ /* empty */ }); 688 } 689 690 /** 691 * Handles the creation or reuse of a ReusableV2 component 692 * 693 * This function is invoked from the transpiler for components declared as ReusableV2. 694 * It manages the lifecycle of components by either creating a new component or reusing 695 * an existing recycle node. 696 * 697 * During the initial render: 698 * - If a recycle node is available, it is reused; otherwise, a new component is created. 699 * - A `ViewV2.createRecycle` call is made to the native side to manage recycling. 700 * - The callback `aboutToReuseInternal` is triggered when a recycled node is used, indicating 701 * the node was fetched and reused instead of being newly created. 702 * 703 * On subsequent renders, state variables are updated for the reused component. 704 * 705 * @param componentClass - The class of the component to be created or reused. 706 * @param getParams - A function returning the parameters for the component. 707 * @param getReuseId - A function providing a unique reuse ID (default: component class name). 708 * @param extraInfo - Additional information required for component creation. 709 */ 710 public reuseOrCreateNewComponent(params: { 711 componentClass: any, getParams: () => Object, 712 getReuseId?: () => string, extraInfo?: ExtraInfo 713 }): void { 714 const { componentClass, getParams, getReuseId = (): string => '', extraInfo } = params; 715 let reuseId = getReuseId(); 716 // If reuseId is null or empty (not set by the application), default to the component's name 717 if (!reuseId) { 718 reuseId = componentClass.name; 719 } 720 this.observeComponentCreation2((elmtId, isInitialRender) => { 721 if (isInitialRender) { 722 const params = getParams(); // should call here to record dependency 723 const recycledNode = this.hasRecyclePool() ? this.getRecyclePool().popRecycleV2Component(reuseId) : null; 724 const componentRef = recycledNode ? recycledNode : 725 new componentClass(/* Parent */this, params, /*localStorage */undefined, elmtId, /*paramsLambda */() => { }, extraInfo); 726 if (recycledNode) { 727 // If a recycled node is found, update the recycled element ID mapping in the recycle pool 728 const lastId = this.recyclePoolV2_.getRecycleIdMapping(recycledNode.id__()); 729 this.recyclePoolV2_.updateRecycleIdMapping(recycledNode.id__(), elmtId); 730 recycledNode.hasBeenRecycled_ = false; 731 732 // Removes the recycled elementId after the recycleId mapping is updated. 733 this.cleanupRecycledElmtId(lastId); // clean useless dependency 734 } 735 736 // Native call to fetch the cached recycle node or create a new one if it doesn't exist 737 ViewV2.createRecycle(componentRef, recycledNode != null, reuseId, () => { 738 // Callback from the native side when the component is reused. 739 recycledNode?.aboutToReuseInternal(params); 740 }); 741 742 // Set the component's parameters generator function for later retrieval during reuse 743 componentRef.paramsGenerator_ = getParams; 744 stateMgmtConsole.debug(`${this.debugInfo__()}: paramsGenerator_:${JSON.stringify(componentRef.paramsGenerator_())}`); 745 } 746 else { 747 // Retrieve the mapped recycled element ID to update the state variables 748 const recycledElmtId = this.getOrCreateRecyclePool().getRecycleIdMapping(elmtId); 749 this.updateStateVarsOfChildByElmtId(recycledElmtId, getParams()); 750 } 751 }, componentClass); 752 } 753 754 public debugInfoDirtDescendantElementIdsInternal(depth: number = 0, recursive: boolean = false, counter: ProfileRecursionCounter): string { 755 let retVaL: string = `\n${' '.repeat(depth)}|--${this.constructor.name}[${this.id__()}]: {`; 756 retVaL += `ViewV2 keeps no info about dirty elmtIds}`; 757 if (recursive) { 758 this.childrenWeakrefMap_.forEach((value, key, map) => { 759 retVaL += value.deref()?.debugInfoDirtDescendantElementIdsInternal(depth + 1, recursive, counter); 760 }); 761 } 762 763 if (recursive && depth === 0) { 764 retVaL += `\nTotal: ${counter.total}`; 765 } 766 return retVaL; 767 } 768 769 770 public debugInfoStateVars(): string { 771 let retVal: string = `|--${this.constructor.name}[${this.id__()}]\n`; 772 let meta = this[ObserveV2.V2_DECO_META]; 773 if (!meta) { 774 retVal += ' No State Variables'; 775 return retVal; 776 } 777 Object.getOwnPropertyNames(meta) 778 .filter((varName) => !varName.startsWith(ProviderConsumerUtilV2.ALIAS_PREFIX)) // remove provider & consumer prefix 779 .forEach((varName) => { 780 const prop: any = Reflect.get(meta, varName); 781 if ('deco' in prop) { 782 retVal += ` ${prop.deco}`; // main decorator 783 } 784 if ('deco2' in prop) { 785 retVal += ` ${prop.deco2}`; // sub decorator like @Once 786 } 787 if ('aliasName' in prop) { 788 retVal += `(${prop.aliasName})`; // aliasName for provider & consumer 789 } 790 retVal += ` varName: ${varName}`; 791 let dependentElmtIds = this[ObserveV2.SYMBOL_REFS][varName]; 792 if (dependentElmtIds) { 793 retVal += `\n |--DependentElements:`; 794 dependentElmtIds.forEach((elmtId) => { 795 if (elmtId < ComputedV2.MIN_COMPUTED_ID) { 796 retVal += ` ` + ObserveV2.getObserve().getElementInfoById(elmtId); 797 } else if (elmtId < MonitorV2.MIN_WATCH_ID) { 798 retVal += ` @Computed[${elmtId}]`; 799 } else if (elmtId < PersistenceV2Impl.MIN_PERSISTENCE_ID) { 800 retVal += ` @Monitor[${elmtId}]`; 801 } else { 802 retVal += ` PersistenceV2[${elmtId}]`; 803 } 804 }); 805 } 806 retVal += '\n'; 807 808 }); 809 return retVal; 810 } 811 812 /** 813 * on first render create a new Instance of Repeat 814 * on re-render connect to existing instance 815 * @param arr 816 * @returns 817 */ 818 public __mkRepeatAPI: <I>(arr: Array<I>) => RepeatAPI<I> = <I>(arr: Array<I>): RepeatAPI<I> => { 819 // factory is for future extensions, currently always return the same 820 const elmtId = ObserveV2.getCurrentRecordedId(); 821 let repeat = this.elmtId2Repeat_.get(elmtId) as __Repeat<I>; 822 if (!repeat) { 823 repeat = new __Repeat<I>(this, arr); 824 this.elmtId2Repeat_.set(elmtId, repeat); 825 } else { 826 repeat.updateArr(arr); 827 } 828 return repeat; 829 }; 830 831 public debugInfoView(recursive: boolean = false): string { 832 return this.debugInfoViewInternal(recursive); 833 } 834 835 private debugInfoViewInternal(recursive: boolean = false): string { 836 let retVal: string = `@ComponentV2\n${this.constructor.name}[${this.id__()}]`; 837 retVal += `\n\nView Hierarchy:\n${this.debugInfoViewHierarchy(recursive)}`; 838 retVal += `\n\nState variables:\n${this.debugInfoStateVars()}`; 839 retVal += `\n\nRegistered Element IDs:\n${this.debugInfoUpdateFuncByElmtId(recursive)}`; 840 retVal += `\n\nDirty Registered Element IDs:\n${this.debugInfoDirtDescendantElementIds(recursive)}`; 841 return retVal; 842 } 843 844 public debugInfoDirtDescendantElementIds(recursive: boolean = false): string { 845 return this.debugInfoDirtDescendantElementIdsInternal(0, recursive, { total: 0 }); 846 } 847 848 public observeRecycleComponentCreation(name: string, recycleUpdateFunc: RecycleUpdateFunc): void { 849 // cannot use ReusableV1 in V2, but for compatibility, do not throw error.. 850 // transpiler will try to give a warning to hint that it will downgrade to normal V1 851 stateMgmtConsole.error(`${this.debugInfo__()}: Recycle not supported for ComponentV2 instance`); 852 } 853} 854