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 136 this.updateFuncByElmtId.clear(); 137 if (this.parent_) { 138 this.parent_.removeChild(this); 139 } 140 } 141 142 public initialRenderView(): void { 143 stateMgmtProfiler.begin(`ViewV2: initialRenderView`); 144 this.initialRender(); 145 stateMgmtProfiler.end(); 146 } 147 148 public observeComponentCreation2(compilerAssignedUpdateFunc: UpdateFunc, classObject: { prototype: Object, pop?: () => void }): void { 149 if (this.isDeleting_) { 150 stateMgmtConsole.error(`@ComponentV2 ${this.constructor.name} elmtId ${this.id__()} is already in process of destruction, will not execute observeComponentCreation2 `); 151 return; 152 } 153 const _componentName: string = (classObject && ('name' in classObject)) ? Reflect.get(classObject, 'name') as string : 'unspecified UINode'; 154 const _popFunc: () => void = (classObject && 'pop' in classObject) ? classObject.pop! : (): void => { }; 155 const updateFunc = (elmtId: number, isFirstRender: boolean): void => { 156 this.syncInstanceId(); 157 stateMgmtConsole.debug(`@ComponentV2 ${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} ${_componentName}[${elmtId}] - start ....`); 158 159 ViewStackProcessor.StartGetAccessRecordingFor(elmtId); 160 ObserveV2.getObserve().startRecordDependencies(this, elmtId); 161 162 compilerAssignedUpdateFunc(elmtId, isFirstRender); 163 if (!isFirstRender) { 164 _popFunc(); 165 } 166 167 let node = this.getNodeById(elmtId); 168 if (node !== undefined) { 169 (node as ArkComponent).cleanStageValue(); 170 } 171 172 ObserveV2.getObserve().stopRecordDependencies(); 173 ViewStackProcessor.StopGetAccessRecording(); 174 175 stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} ${_componentName}[${elmtId}] - DONE ....`); 176 this.restoreInstanceId(); 177 }; 178 179 const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent(); 180 // needs to move set before updateFunc. 181 // make sure the key and object value exist since it will add node in attributeModifier during updateFunc. 182 this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc, classObject: classObject }); 183 // add element id -> owning ViewV2 184 UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this)); 185 try { 186 updateFunc(elmtId, /* is first render */ true); 187 } catch (error) { 188 // avoid the incompatible change that move set function before updateFunc. 189 this.updateFuncByElmtId.delete(elmtId); 190 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId); 191 stateMgmtConsole.applicationError(`${this.debugInfo__()} has error in update func: ${(error as Error).message}`); 192 throw error; 193 } 194 stateMgmtConsole.debug(`${this.debugInfo__()} is initial rendering elmtId ${elmtId}, tag: ${_componentName}, and updateFuncByElmtId size :${this.updateFuncByElmtId.size}`); 195 } 196 197 /** 198 * 199 * @param paramVariableName 200 * @param @once paramVariableName 201 * @param is read only, therefore, init from parent needs to be done without 202 * causing property setter() to be called 203 * @param newValue 204 */ 205 protected initParam<Z>(paramVariableName: string, newValue: Z): void { 206 this.checkIsV1Proxy(paramVariableName, newValue); 207 VariableUtilV3.initParam<Z>(this, paramVariableName, newValue); 208 } 209 /** 210 * 211 * @param paramVariableName 212 * @param @once paramVariableName 213 * @param is read only, therefore, update from parent needs to be done without 214 * causing property setter() to be called 215 * @param @once reject any update 216 * @param newValue 217 */ 218 protected updateParam<Z>(paramVariableName: string, newValue: Z): void { 219 this.checkIsV1Proxy(paramVariableName, newValue); 220 VariableUtilV3.updateParam<Z>(this, paramVariableName, newValue); 221 } 222 223 private checkIsV1Proxy<Z>(paramVariableName: string, value: Z): void { 224 if (ObservedObject.IsObservedObject(value)) { 225 throw new Error(`Cannot assign the ComponentV1 value to the ComponentV2 for the property '${paramVariableName}'`); 226 } 227 } 228 229 /** 230 * inform that UINode with given elmtId needs rerender 231 * does NOT exec @Watch function. 232 * only used on V3 code path from ObserveV2.fireChange. 233 * 234 * FIXME will still use in the future? 235 */ 236 public uiNodeNeedUpdateV3(elmtId: number): void { 237 if (this.isFirstRender()) { 238 return; 239 } 240 241 stateMgmtProfiler.begin(`ViewV2.uiNodeNeedUpdate ${this.debugInfoElmtId(elmtId)}`); 242 243 if (!this.isActive_) { 244 this.scheduleDelayedUpdate(elmtId); 245 return; 246 } 247 248 if (!this.dirtDescendantElementIds_.size) { // && !this runReuse_) { 249 // mark ComposedElement dirty when first elmtIds are added 250 // do not need to do this every time 251 this.syncInstanceId(); 252 this.markNeedUpdate(); 253 this.restoreInstanceId(); 254 } 255 this.dirtDescendantElementIds_.add(elmtId); 256 stateMgmtConsole.debug(`${this.debugInfo__()}: uiNodeNeedUpdate: updated full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`); 257 258 stateMgmtProfiler.end(); 259 } 260 261 262 /** 263 * For each recorded dirty Element in this custom component 264 * run its update function 265 * 266 */ 267 public updateDirtyElements(): void { 268 stateMgmtProfiler.begin('ViewV2.updateDirtyElements'); 269 do { 270 stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render): sorted dirty elmtIds: ${Array.from(this.dirtDescendantElementIds_).sort(ViewV2.compareNumber)}, starting ....`); 271 272 // see which elmtIds are managed by this View 273 // and clean up all book keeping for them 274 this.purgeDeletedElmtIds(); 275 276 // process all elmtIds marked as needing update in ascending order. 277 // ascending order ensures parent nodes will be updated before their children 278 // prior cleanup ensure no already deleted Elements have their update func executed 279 const dirtElmtIdsFromRootNode = Array.from(this.dirtDescendantElementIds_).sort(ViewV2.compareNumber); 280 // if state changed during exec update lambda inside UpdateElement, then the dirty elmtIds will be added 281 // to newly created this.dirtDescendantElementIds_ Set 282 dirtElmtIdsFromRootNode.forEach(elmtId => { 283 this.UpdateElement(elmtId); 284 this.dirtDescendantElementIds_.delete(elmtId); 285 }); 286 287 if (this.dirtDescendantElementIds_.size) { 288 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!`); 289 } 290 } while (this.dirtDescendantElementIds_.size); 291 stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render) - DONE`); 292 stateMgmtProfiler.end(); 293 } 294 295 296 public UpdateElement(elmtId: number): void { 297 298 if (this.isDeleting_) { 299 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement(${elmtId}) (V2) returns with NO UPDATE, this @ComponentV2 is under deletion!`); 300 return; 301 } 302 303 stateMgmtProfiler.begin('ViewV2.UpdateElement'); 304 if (elmtId === this.id__()) { 305 // do not attempt to update itself 306 stateMgmtProfiler.end(); 307 return; 308 } 309 // do not process an Element that has been marked to be deleted 310 const entry: UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId); 311 const updateFunc = entry ? entry.getUpdateFunc() : undefined; 312 313 if (typeof updateFunc !== 'function') { 314 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: update function of elmtId ${elmtId} not found, internal error!`); 315 } else { 316 const componentName = entry.getComponentName(); 317 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${componentName} elmtId ${elmtId} start ...`); 318 stateMgmtProfiler.begin('ViewV2.updateFunc'); 319 try { 320 updateFunc(elmtId, /* isFirstRender */ false); 321 } catch (e) { 322 stateMgmtConsole.applicationError(`Exception caught in update function of ${componentName} for elmtId ${elmtId}`, e.toString()); 323 throw e; 324 } finally { 325 stateMgmtProfiler.end(); 326 } 327 stateMgmtProfiler.begin('ViewV2.finishUpdateFunc (native)'); 328 this.finishUpdateFunc(elmtId); 329 stateMgmtProfiler.end(); 330 stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${componentName} elmtId ${elmtId} - DONE`); 331 } 332 stateMgmtProfiler.end(); 333 } 334 335 /** 336 * Retrieve child by given id 337 * @param id 338 * @returns child if child with this id exists and it is instance of ViewV2 339 */ 340 public getViewV2ChildById(id: number): ViewV2 | undefined { 341 const childWeakRef = this.childrenWeakrefMap_.get(id); 342 const child = childWeakRef ? childWeakRef.deref() : undefined; 343 return (child && child instanceof ViewV2) ? child : undefined; 344 } 345 346 /** 347 * findViewPUInHierarchy function needed for @Component and @ComponentV2 mixed 348 * parent - child hierarchies. Not used by ViewV2 349 */ 350 public findViewPUInHierarchy(id: number): ViewPU | undefined { 351 // this ViewV2 is not a ViewPU, continue searching amongst children 352 let retVal: ViewPU = undefined; 353 for (const [key, value] of this.childrenWeakrefMap_.entries()) { 354 retVal = value.deref().findViewPUInHierarchy(id); 355 if (retVal) { 356 break; 357 } 358 } 359 return retVal; 360 } 361 362 // WatchIds that needs to be fired later gets added to monitorIdsDelayedUpdate 363 // monitor fireChange will be triggered for all these watchIds once this view gets active 364 public addDelayedMonitorIds(watchId: number): void { 365 stateMgmtConsole.debug(`${this.debugInfo__()} addDelayedMonitorIds called for watchId: ${watchId}`); 366 this.monitorIdsDelayedUpdate.add(watchId); 367 } 368 369 public addDelayedComputedIds(watchId: number): void { 370 stateMgmtConsole.debug(`${this.debugInfo__()} addDelayedComputedIds called for watchId: ${watchId}`); 371 this.computedIdsDelayedUpdate.add(watchId); 372 } 373 374 public setActiveInternal(newState: boolean): void { 375 stateMgmtProfiler.begin('ViewV2.setActive'); 376 377 if (!this.isCompFreezeAllowed()) { 378 stateMgmtConsole.debug(`${this.debugInfo__()}: ViewV2.setActive. Component freeze state is ${this.isCompFreezeAllowed()} - ignoring`); 379 stateMgmtProfiler.end(); 380 return; 381 } 382 383 stateMgmtConsole.debug(`${this.debugInfo__()}: ViewV2.setActive ${newState ? ' inActive -> active' : 'active -> inActive'}`); 384 this.isActive_ = newState; 385 if (this.isActive_) { 386 this.onActiveInternal(); 387 } else { 388 this.onInactiveInternal(); 389 } 390 stateMgmtProfiler.end(); 391 } 392 393 private onActiveInternal(): void { 394 if (!this.isActive_) { 395 return; 396 } 397 398 stateMgmtConsole.debug(`${this.debugInfo__()}: onActiveInternal`); 399 this.performDelayedUpdate(); 400 401 // Set 'isActive_' state for all descendant child Views 402 for (const child of this.childrenWeakrefMap_.values()) { 403 const childView: IView | undefined = child.deref(); 404 if (childView) { 405 childView.setActiveInternal(this.isActive_); 406 } 407 } 408 } 409 410 private onInactiveInternal(): void { 411 if (this.isActive_) { 412 return; 413 } 414 stateMgmtConsole.debug(`${this.debugInfo__()}: onInactiveInternal`); 415 416 // Set 'isActive_' state for all descendant child Views 417 for (const child of this.childrenWeakrefMap_.values()) { 418 const childView: IView | undefined = child.deref(); 419 if (childView) { 420 childView.setActiveInternal(this.isActive_); 421 } 422 } 423 } 424 425 private performDelayedUpdate(): void { 426 stateMgmtProfiler.begin('ViewV2: performDelayedUpdate'); 427 if (this.computedIdsDelayedUpdate.size) { 428 // exec computed functions 429 ObserveV2.getObserve().updateDirtyComputedProps([...this.computedIdsDelayedUpdate]); 430 } 431 if (this.monitorIdsDelayedUpdate.size) { 432 // exec monitor functions 433 ObserveV2.getObserve().updateDirtyMonitors(this.monitorIdsDelayedUpdate); 434 } 435 if (this.elmtIdsDelayedUpdate.size) { 436 // update re-render of updated element ids once the view gets active 437 if (this.dirtDescendantElementIds_.size === 0) { 438 this.dirtDescendantElementIds_ = new Set(this.elmtIdsDelayedUpdate); 439 } 440 else { 441 this.elmtIdsDelayedUpdate.forEach((element) => { 442 this.dirtDescendantElementIds_.add(element); 443 }); 444 } 445 } 446 this.markNeedUpdate(); 447 this.elmtIdsDelayedUpdate.clear(); 448 this.monitorIdsDelayedUpdate.clear(); 449 this.computedIdsDelayedUpdate.clear(); 450 stateMgmtProfiler.end(); 451 } 452 453 /* 454 findProvidePU finds @Provided property recursively by traversing ViewPU's towards that of the UI tree root @Component: 455 if 'this' ViewPU has a @Provide('providedPropName') return it, otherwise ask from its parent ViewPU. 456 function needed for mixed @Component and @ComponentV2 parent child hierarchies. 457 */ 458 public findProvidePU(providedPropName: string): ObservedPropertyAbstractPU<any> | undefined { 459 return this.getParent()?.findProvidePU(providedPropName); 460 } 461 462 get localStorage_(): LocalStorage { 463 // FIXME check this also works for root @ComponentV2 464 return (this.getParent()) ? this.getParent().localStorage_ : new LocalStorage({ /* empty */ }); 465 } 466 467 /** 468 * @function observeRecycleComponentCreation 469 * @description custom node recycle creation not supported for V2. So a dummy function is implemented to report 470 * an error message 471 * @param name custom node name 472 * @param recycleUpdateFunc custom node recycle update which can be converted to a normal update function 473 * @return void 474 */ 475 public observeRecycleComponentCreation(name: string, recycleUpdateFunc: RecycleUpdateFunc): void { 476 stateMgmtConsole.error(`${this.debugInfo__()}: Recycle not supported for ComponentV2 instances`); 477 } 478 479 public debugInfoDirtDescendantElementIdsInternal(depth: number = 0, recursive: boolean = false, counter: ProfileRecursionCounter): string { 480 let retVaL: string = `\n${' '.repeat(depth)}|--${this.constructor.name}[${this.id__()}]: {`; 481 retVaL += `ViewV2 keeps no info about dirty elmtIds`; 482 if (recursive) { 483 this.childrenWeakrefMap_.forEach((value, key, map) => { 484 retVaL += value.deref()?.debugInfoDirtDescendantElementIdsInternal(depth + 1, recursive, counter); 485 }); 486 } 487 488 if (recursive && depth === 0) { 489 retVaL += `\nTotal: ${counter.total}`; 490 } 491 return retVaL; 492 } 493 494 495 protected debugInfoStateVars(): string { 496 return ''; // TODO DFX, read out META 497 } 498 499 /** 500 * on first render create a new Instance of Repeat 501 * on re-render connect to existing instance 502 * @param arr 503 * @returns 504 */ 505 public __mkRepeatAPI: <I>(arr: Array<I>) => RepeatAPI<I> = <I>(arr: Array<I>): RepeatAPI<I> => { 506 // factory is for future extensions, currently always return the same 507 const elmtId = ObserveV2.getCurrentRecordedId(); 508 let repeat = this.elmtId2Repeat_.get(elmtId) as __Repeat<I>; 509 if (!repeat) { 510 repeat = new __Repeat<I>(this, arr); 511 this.elmtId2Repeat_.set(elmtId, repeat); 512 } else { 513 repeat.updateArr(arr); 514 } 515 return repeat; 516 }; 517} 518