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 * This file includes only framework internal classes and functions 19 * non are part of SDK. Do not access from app. 20 * 21 * Implementation of @ComponentV2 is ViewV2 22 * When transpiling @ComponentV2, the transpiler generates a class that extends from ViewV2. 23 * 24 */ 25 26abstract class ViewV2 extends PUV2ViewBase implements IView { 27 28 // Set of elmtIds that need re-render 29 protected dirtDescendantElementIds_: Set<number> = new Set<number>(); 30 31 private monitorIdsDelayedUpdate: Set<number> = new Set(); 32 private computedIdsDelayedUpdate: Set<number> = new Set(); 33 34 constructor(parent: IView, elmtId: number = UINodeRegisterProxy.notRecordingDependencies, extraInfo: ExtraInfo = undefined) { 35 super(parent, elmtId, extraInfo); 36 this.setIsV2(true); 37 stateMgmtConsole.debug(`ViewV2 constructor: Creating @Component '${this.constructor.name}' from parent '${parent?.constructor.name}'`); 38 } 39 40 41 /** 42 * The `freezeState` parameter determines whether this @ComponentV2 is allowed to freeze, when inactive 43 * Its called with value of the `freezeWhenInactive` parameter from the @ComponentV2 decorator, 44 * or it may be called with `undefined` depending on how the UI compiler works. 45 * 46 * @param freezeState Only the value `true` will be used to set the freeze state, 47 * otherwise it inherits from its parent instance if its freezeState is true 48 */ 49 protected finalizeConstruction(freezeState?: boolean | undefined): void { 50 51 ObserveV2.getObserve().constructComputed(this, this.constructor.name); 52 ObserveV2.getObserve().constructMonitor(this, this.constructor.name); 53 54 // Always use ID_REFS in ViewV2 55 this[ObserveV2.ID_REFS] = {}; 56 57 // set to true if freeze parameter set for this @ComponentV2 to true 58 // otherwise inherit from its parentComponent (if it exists). 59 this.isCompFreezeAllowed_ = freezeState || this.isCompFreezeAllowed_; 60 stateMgmtConsole.debug(`${this.debugInfo__()}: @ComponentV2 freezeWhenInactive state is set to ${this.isCompFreezeAllowed()}`); 61 62 } 63 64 public debugInfo__(): string { 65 return `@ComponentV2 '${this.constructor.name}'[${this.id__()}]`; 66 } 67 68 69 private get isViewV3(): boolean { 70 return true; 71 } 72 73 /** 74 * Virtual function implemented in ViewPU and ViewV2 75 * Unregisters and purges all child elements associated with the specified Element ID in ViewV2. 76 * 77 * @param rmElmtId - The Element ID to be purged and deleted 78 * @returns {boolean} - Returns `true` if the Element ID was successfully deleted, `false` otherwise. 79 */ 80 public purgeDeleteElmtId(rmElmtId: number): boolean { 81 stateMgmtConsole.debug(`${this.debugInfo__()} purgeDeleteElmtId (V2) is purging the rmElmtId:${rmElmtId}`); 82 const result = this.updateFuncByElmtId.delete(rmElmtId); 83 if (result) { 84 const childOpt = this.getChildViewV2ForElmtId(rmElmtId); 85 if (childOpt) { 86 childOpt.setDeleting(); 87 childOpt.setDeleteStatusRecursively(); 88 } 89 90 // it means rmElmtId has finished all the unregistration from the js side, ElementIdToOwningViewPU_ does not need to keep it 91 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(rmElmtId); 92 } 93 94 // Needed only for V2 95 ObserveV2.getObserve().clearBinding(rmElmtId); 96 return result; 97 } 98 99 100 // super class will call this function from 101 // its aboutToBeDeleted implementation 102 protected aboutToBeDeletedInternal(): void { 103 stateMgmtConsole.debug(`${this.debugInfo__()}: aboutToBeDeletedInternal`); 104 // if this isDeleting_ is true already, it may be set delete status recursively by its parent, so it is not necessary 105 // to set and resursively set its children any more 106 if (!this.isDeleting_) { 107 this.isDeleting_ = true; 108 this.setDeleteStatusRecursively(); 109 } 110 // tell UINodeRegisterProxy that all elmtIds under 111 // this ViewV2 should be treated as already unregistered 112 113 stateMgmtConsole.debug(`${this.constructor.name}: aboutToBeDeletedInternal `); 114 115 // purge the elmtIds owned by this ViewV2 from the updateFuncByElmtId and also the state variable dependent elmtIds 116 Array.from(this.updateFuncByElmtId.keys()).forEach((elmtId: number) => { 117 // FIXME split View: enable delete this purgeDeleteElmtId(elmtId); 118 }); 119 120 // unregistration of ElementIDs 121 stateMgmtConsole.debug(`${this.debugInfo__()}: onUnRegElementID`); 122 123 // it will unregister removed elementids from all the ViewV2, equals purgeDeletedElmtIdsRecursively 124 this.purgeDeletedElmtIds(); 125 126 // unregisters its own id once its children are unregistered above 127 UINodeRegisterProxy.unregisterRemovedElmtsFromViewPUs([this.id__()]); 128 129 stateMgmtConsole.debug(`${this.debugInfo__()}: onUnRegElementID - DONE`); 130 131 /* in case ViewPU is currently frozen 132 ViewPU inactiveComponents_ delete(`${this.constructor.name}[${this.id__()}]`); 133 */ 134 MonitorV2.clearWatchesFromTarget(this); 135 ComputedV2.clearComputedFromTarget(this); 136 137 this.updateFuncByElmtId.clear(); 138 if (this.parent_) { 139 this.parent_.removeChild(this); 140 } 141 } 142 143 public initialRenderView(): void { 144 stateMgmtProfiler.begin(`ViewV2: initialRenderView`); 145 this.initialRender(); 146 stateMgmtProfiler.end(); 147 } 148 149 public observeComponentCreation2(compilerAssignedUpdateFunc: UpdateFunc, classObject: { prototype: Object, pop?: () => void }): void { 150 if (this.isDeleting_) { 151 stateMgmtConsole.error(`@ComponentV2 ${this.constructor.name} elmtId ${this.id__()} is already in process of destruction, will not execute observeComponentCreation2 `); 152 return; 153 } 154 const _componentName: string = (classObject && ('name' in classObject)) ? Reflect.get(classObject, 'name') as string : 'unspecified UINode'; 155 const _popFunc: () => void = (classObject && 'pop' in classObject) ? classObject.pop! : (): void => { }; 156 const updateFunc = (elmtId: number, isFirstRender: boolean): void => { 157 this.syncInstanceId(); 158 stateMgmtConsole.debug(`@ComponentV2 ${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} ${_componentName}[${elmtId}] - start ....`); 159 160 ViewStackProcessor.StartGetAccessRecordingFor(elmtId); 161 ObserveV2.getObserve().startRecordDependencies(this, elmtId); 162 163 compilerAssignedUpdateFunc(elmtId, isFirstRender); 164 if (!isFirstRender) { 165 _popFunc(); 166 } 167 168 let node = this.getNodeById(elmtId); 169 if (node !== undefined) { 170 (node as ArkComponent).cleanStageValue(); 171 } 172 173 ObserveV2.getObserve().stopRecordDependencies(); 174 ViewStackProcessor.StopGetAccessRecording(); 175 176 stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} ${_componentName}[${elmtId}] - DONE ....`); 177 this.restoreInstanceId(); 178 }; 179 180 const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent(); 181 // needs to move set before updateFunc. 182 // make sure the key and object value exist since it will add node in attributeModifier during updateFunc. 183 this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc, classObject: classObject }); 184 // add element id -> owning ViewV2 185 UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this)); 186 try { 187 updateFunc(elmtId, /* is first render */ true); 188 } catch (error) { 189 // avoid the incompatible change that move set function before updateFunc. 190 this.updateFuncByElmtId.delete(elmtId); 191 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId); 192 stateMgmtConsole.applicationError(`${this.debugInfo__()} has error in update func: ${(error as Error).message}`); 193 throw error; 194 } 195 stateMgmtConsole.debug(`${this.debugInfo__()} is initial rendering elmtId ${elmtId}, tag: ${_componentName}, and updateFuncByElmtId size :${this.updateFuncByElmtId.size}`); 196 } 197 198 /** 199 * 200 * @param paramVariableName 201 * @param @once paramVariableName 202 * @param is read only, therefore, init from parent needs to be done without 203 * causing property setter() to be called 204 * @param newValue 205 */ 206 protected initParam<Z>(paramVariableName: string, newValue: Z): void { 207 this.checkIsV1Proxy(paramVariableName, newValue); 208 VariableUtilV3.initParam<Z>(this, paramVariableName, newValue); 209 } 210 /** 211 * 212 * @param paramVariableName 213 * @param @once paramVariableName 214 * @param is read only, therefore, update from parent needs to be done without 215 * causing property setter() to be called 216 * @param @once reject any update 217 * @param newValue 218 */ 219 protected updateParam<Z>(paramVariableName: string, newValue: Z): void { 220 this.checkIsV1Proxy(paramVariableName, newValue); 221 VariableUtilV3.updateParam<Z>(this, paramVariableName, newValue); 222 } 223 224 private checkIsV1Proxy<Z>(paramVariableName: string, value: Z): void { 225 if (ObservedObject.IsObservedObject(value)) { 226 throw new Error(`Cannot assign the ComponentV1 value to the ComponentV2 for the property '${paramVariableName}'`); 227 } 228 } 229 230 /** 231 * inform that UINode with given elmtId needs rerender 232 * does NOT exec @Watch function. 233 * only used on V3 code path from ObserveV2.fireChange. 234 * 235 * FIXME will still use in the future? 236 */ 237 public uiNodeNeedUpdateV3(elmtId: number): void { 238 if (this.isFirstRender()) { 239 return; 240 } 241 242 stateMgmtProfiler.begin(`ViewV2.uiNodeNeedUpdate ${this.debugInfoElmtId(elmtId)}`); 243 244 if (!this.isActive_) { 245 this.scheduleDelayedUpdate(elmtId); 246 return; 247 } 248 249 if (!this.dirtDescendantElementIds_.size) { // && !this runReuse_) { 250 // mark ComposedElement dirty when first elmtIds are added 251 // do not need to do this every time 252 this.syncInstanceId(); 253 this.markNeedUpdate(); 254 this.restoreInstanceId(); 255 } 256 this.dirtDescendantElementIds_.add(elmtId); 257 stateMgmtConsole.debug(`${this.debugInfo__()}: uiNodeNeedUpdate: updated full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`); 258 259 stateMgmtProfiler.end(); 260 } 261 262 263 /** 264 * For each recorded dirty Element in this custom component 265 * run its update function 266 * 267 */ 268 public updateDirtyElements(): void { 269 stateMgmtProfiler.begin('ViewV2.updateDirtyElements'); 270 do { 271 stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render): sorted dirty elmtIds: ${Array.from(this.dirtDescendantElementIds_).sort(ViewV2.compareNumber)}, starting ....`); 272 273 // see which elmtIds are managed by this View 274 // and clean up all book keeping for them 275 this.purgeDeletedElmtIds(); 276 277 // process all elmtIds marked as needing update in ascending order. 278 // ascending order ensures parent nodes will be updated before their children 279 // prior cleanup ensure no already deleted Elements have their update func executed 280 const dirtElmtIdsFromRootNode = Array.from(this.dirtDescendantElementIds_).sort(ViewV2.compareNumber); 281 // if state changed during exec update lambda inside UpdateElement, then the dirty elmtIds will be added 282 // to newly created this.dirtDescendantElementIds_ Set 283 dirtElmtIdsFromRootNode.forEach(elmtId => { 284 this.UpdateElement(elmtId); 285 this.dirtDescendantElementIds_.delete(elmtId); 286 }); 287 288 if (this.dirtDescendantElementIds_.size) { 289 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!`); 290 } 291 } while (this.dirtDescendantElementIds_.size); 292 stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render) - DONE`); 293 stateMgmtProfiler.end(); 294 } 295 296 297 public UpdateElement(elmtId: number): void { 298 299 if (this.isDeleting_) { 300 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement(${elmtId}) (V2) returns with NO UPDATE, this @ComponentV2 is under deletion!`); 301 return; 302 } 303 304 stateMgmtProfiler.begin('ViewV2.UpdateElement'); 305 if (elmtId === this.id__()) { 306 // do not attempt to update itself 307 stateMgmtProfiler.end(); 308 return; 309 } 310 // do not process an Element that has been marked to be deleted 311 const entry: UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId); 312 const updateFunc = entry ? entry.getUpdateFunc() : undefined; 313 314 if (typeof updateFunc !== 'function') { 315 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: update function of elmtId ${elmtId} not found, internal error!`); 316 } else { 317 const componentName = entry.getComponentName(); 318 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${componentName} elmtId ${elmtId} start ...`); 319 stateMgmtProfiler.begin('ViewV2.updateFunc'); 320 try { 321 updateFunc(elmtId, /* isFirstRender */ false); 322 } catch (e) { 323 stateMgmtConsole.applicationError(`Exception caught in update function of ${componentName} for elmtId ${elmtId}`, e.toString()); 324 throw e; 325 } finally { 326 stateMgmtProfiler.end(); 327 } 328 stateMgmtProfiler.begin('ViewV2.finishUpdateFunc (native)'); 329 this.finishUpdateFunc(elmtId); 330 stateMgmtProfiler.end(); 331 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${componentName} elmtId ${elmtId} - DONE`); 332 } 333 stateMgmtProfiler.end(); 334 } 335 336 /** 337 * Retrieve child by given id 338 * @param id 339 * @returns child if child with this id exists and it is instance of ViewV2 340 */ 341 public getViewV2ChildById(id: number): ViewV2 | undefined { 342 const childWeakRef = this.childrenWeakrefMap_.get(id); 343 const child = childWeakRef ? childWeakRef.deref() : undefined; 344 return (child && child instanceof ViewV2) ? child : undefined; 345 } 346 347 /** 348 * findViewPUInHierarchy function needed for @Component and @ComponentV2 mixed 349 * parent - child hierarchies. Not used by ViewV2 350 */ 351 public findViewPUInHierarchy(id: number): ViewPU | undefined { 352 // this ViewV2 is not a ViewPU, continue searching amongst children 353 let retVal: ViewPU = undefined; 354 for (const [key, value] of this.childrenWeakrefMap_.entries()) { 355 retVal = value.deref().findViewPUInHierarchy(id); 356 if (retVal) { 357 break; 358 } 359 } 360 return retVal; 361 } 362 363 // WatchIds that needs to be fired later gets added to monitorIdsDelayedUpdate 364 // monitor fireChange will be triggered for all these watchIds once this view gets active 365 public addDelayedMonitorIds(watchId: number): void { 366 stateMgmtConsole.debug(`${this.debugInfo__()} addDelayedMonitorIds called for watchId: ${watchId}`); 367 this.monitorIdsDelayedUpdate.add(watchId); 368 } 369 370 public addDelayedComputedIds(watchId: number): void { 371 stateMgmtConsole.debug(`${this.debugInfo__()} addDelayedComputedIds called for watchId: ${watchId}`); 372 this.computedIdsDelayedUpdate.add(watchId); 373 } 374 375 public setActiveInternal(newState: boolean): void { 376 stateMgmtProfiler.begin('ViewV2.setActive'); 377 378 if (!this.isCompFreezeAllowed()) { 379 stateMgmtConsole.debug(`${this.debugInfo__()}: ViewV2.setActive. Component freeze state is ${this.isCompFreezeAllowed()} - ignoring`); 380 stateMgmtProfiler.end(); 381 return; 382 } 383 384 stateMgmtConsole.debug(`${this.debugInfo__()}: ViewV2.setActive ${newState ? ' inActive -> active' : 'active -> inActive'}`); 385 this.isActive_ = newState; 386 if (this.isActive_) { 387 this.onActiveInternal(); 388 } else { 389 this.onInactiveInternal(); 390 } 391 stateMgmtProfiler.end(); 392 } 393 394 private onActiveInternal(): void { 395 if (!this.isActive_) { 396 return; 397 } 398 399 stateMgmtConsole.debug(`${this.debugInfo__()}: onActiveInternal`); 400 this.performDelayedUpdate(); 401 402 // Set 'isActive_' state for all descendant child Views 403 for (const child of this.childrenWeakrefMap_.values()) { 404 const childView: IView | undefined = child.deref(); 405 if (childView) { 406 childView.setActiveInternal(this.isActive_); 407 } 408 } 409 } 410 411 private onInactiveInternal(): void { 412 if (this.isActive_) { 413 return; 414 } 415 stateMgmtConsole.debug(`${this.debugInfo__()}: onInactiveInternal`); 416 417 // Set 'isActive_' state for all descendant child Views 418 for (const child of this.childrenWeakrefMap_.values()) { 419 const childView: IView | undefined = child.deref(); 420 if (childView) { 421 childView.setActiveInternal(this.isActive_); 422 } 423 } 424 } 425 426 private performDelayedUpdate(): void { 427 stateMgmtProfiler.begin('ViewV2: performDelayedUpdate'); 428 if (this.computedIdsDelayedUpdate.size) { 429 // exec computed functions 430 ObserveV2.getObserve().updateDirtyComputedProps([...this.computedIdsDelayedUpdate]); 431 } 432 if (this.monitorIdsDelayedUpdate.size) { 433 // exec monitor functions 434 ObserveV2.getObserve().updateDirtyMonitors(this.monitorIdsDelayedUpdate); 435 } 436 if (this.elmtIdsDelayedUpdate.size) { 437 // update re-render of updated element ids once the view gets active 438 if (this.dirtDescendantElementIds_.size === 0) { 439 this.dirtDescendantElementIds_ = new Set(this.elmtIdsDelayedUpdate); 440 } 441 else { 442 this.elmtIdsDelayedUpdate.forEach((element) => { 443 this.dirtDescendantElementIds_.add(element); 444 }); 445 } 446 } 447 this.markNeedUpdate(); 448 this.elmtIdsDelayedUpdate.clear(); 449 this.monitorIdsDelayedUpdate.clear(); 450 this.computedIdsDelayedUpdate.clear(); 451 stateMgmtProfiler.end(); 452 } 453 454 /* 455 findProvidePU finds @Provided property recursively by traversing ViewPU's towards that of the UI tree root @Component: 456 if 'this' ViewPU has a @Provide('providedPropName') return it, otherwise ask from its parent ViewPU. 457 function needed for mixed @Component and @ComponentV2 parent child hierarchies. 458 */ 459 public findProvidePU(providedPropName: string): ObservedPropertyAbstractPU<any> | undefined { 460 return this.getParent()?.findProvidePU(providedPropName); 461 } 462 463 get localStorage_(): LocalStorage { 464 // FIXME check this also works for root @ComponentV2 465 return (this.getParent()) ? this.getParent().localStorage_ : new LocalStorage({ /* empty */ }); 466 } 467 468 /** 469 * @function observeRecycleComponentCreation 470 * @description custom node recycle creation not supported for V2. So a dummy function is implemented to report 471 * an error message 472 * @param name custom node name 473 * @param recycleUpdateFunc custom node recycle update which can be converted to a normal update function 474 * @return void 475 */ 476 public observeRecycleComponentCreation(name: string, recycleUpdateFunc: RecycleUpdateFunc): void { 477 stateMgmtConsole.error(`${this.debugInfo__()}: Recycle not supported for ComponentV2 instances`); 478 } 479 480 public debugInfoDirtDescendantElementIdsInternal(depth: number = 0, recursive: boolean = false, counter: ProfileRecursionCounter): string { 481 let retVaL: string = `\n${' '.repeat(depth)}|--${this.constructor.name}[${this.id__()}]: {`; 482 retVaL += `ViewV2 keeps no info about dirty elmtIds`; 483 if (recursive) { 484 this.childrenWeakrefMap_.forEach((value, key, map) => { 485 retVaL += value.deref()?.debugInfoDirtDescendantElementIdsInternal(depth + 1, recursive, counter); 486 }); 487 } 488 489 if (recursive && depth === 0) { 490 retVaL += `\nTotal: ${counter.total}`; 491 } 492 return retVaL; 493 } 494 495 496 protected debugInfoStateVars(): string { 497 return ''; // TODO DFX, read out META 498 } 499 500 /** 501 * on first render create a new Instance of Repeat 502 * on re-render connect to existing instance 503 * @param arr 504 * @returns 505 */ 506 public __mkRepeatAPI: <I>(arr: Array<I>) => RepeatAPI<I> = <I>(arr: Array<I>): RepeatAPI<I> => { 507 // factory is for future extensions, currently always return the same 508 const elmtId = ObserveV2.getCurrentRecordedId(); 509 let repeat = this.elmtId2Repeat_.get(elmtId) as __Repeat<I>; 510 if (!repeat) { 511 repeat = new __Repeat<I>(this, arr); 512 this.elmtId2Repeat_.set(elmtId, repeat); 513 } else { 514 repeat.updateArr(arr); 515 } 516 return repeat; 517 }; 518} 519