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