1# Freezing a Custom Component 2 3Freezing a custom component is designed to optimize the performance of complex UI pages, especially for scenarios where multiple page stacks, long lists, or grid layouts are involved. In these cases, when the state variable is bound to multiple UI components, the change of the state variables may trigger the re-render of a large number of UI components, resulting in frame freezing and response delay. To improve the UI re-render performance, you can try to use the custom component freezing function. 4 5Principles of freezing a component are as follows: 61. Setting the **freezeWhenInactive** attribute to activate the component freezing mechanism. 72. After this function is enabled, the system re-renders only the activated custom components. In this way, the UI framework can narrow down the re-render scope to the (activated) custom components that are visible to users, improving the re-render efficiency in complex UI scenarios. 83. When an inactive custom component turns into the active state, the state management framework performs necessary re-render operations on the custom component to ensure that the UI is correctly displayed. 9 10In short, component freezing aims to optimize UI re-render performance on complex UIs. When there are multiple invisible custom components, such as multiple page stacks, long lists, or grids, you can freeze the components to re-render visible custom components as required, and the re-render of the invisible custom components is delayed until they become visible. 11 12Note that the active or inactive state of a component is not equivalent to its visibility. Component freezing applies only to the following scenarios: 13 141. Page routing: The current top page of the navigation stack is in the active state, and the non-top invisible page is in the inactive state. 152. TabContent: Only the custom component in the currently displayed TabContent is in the active state. 163. LazyForEach: Only the custom component in the currently displayed LazyForEach is in the active state, and the component of the cache node is in the inactive state. 174. Navigation: Only the custom component in the currently displayed NavDestination is in the active state. 185. Component reuse: The component that enters the reuse pool is in the inactive state, and the node attached from the reuse pool is in the active state. 19In other scenarios, for example, masked components in a stack layout are not considered to be in an inactive state although they are invisible. Therefore, component freezing cannot be applied to these components. 20 21Before reading this topic, you are advised to read [Creating a Custom Component](./arkts-create-custom-components.md) to learn about the basic syntax. 22 23> **NOTE** 24> 25> Custom component freezing is supported since API version 11. 26 27## Use Scenarios 28 29### Page Routing 30 31> **NOTE** 32> 33> This example uses router for page redirection but you are advised to use the **Navigation** component instead, because **Navigation** provides more functions and more flexible customization capabilities. For details, see the use cases of [Navigation](#navigation). 34 35When page 1 calls the **router.pushUrl** API to jump to page 2, page 1 is hidden and invisible. In this case, if the state variable on page 1 is updated, page 1 is not re-rendered. 36For details, see the following. 37 38 39 40Page 1 41 42```ts 43import { router } from '@kit.ArkUI'; 44 45@Entry 46@Component({ freezeWhenInactive: true }) 47struct Page1 { 48 @StorageLink('PropA') @Watch("first") storageLink: number = 47; 49 50 first() { 51 console.info("first page " + `${this.storageLink}`) 52 } 53 54 build() { 55 Column() { 56 Text(`From first Page ${this.storageLink}`).fontSize(50) 57 Button('first page storageLink + 1').fontSize(30) 58 .onClick(() => { 59 this.storageLink += 1 60 }) 61 Button('go to next page').fontSize(30) 62 .onClick(() => { 63 router.pushUrl({ url: 'pages/Page2' }) 64 }) 65 } 66 } 67} 68``` 69 70Page 2 71 72```ts 73import { router } from '@kit.ArkUI'; 74 75@Entry 76@Component({ freezeWhenInactive: true }) 77struct Page2 { 78 @StorageLink('PropA') @Watch("second") storageLink2: number = 1; 79 80 second() { 81 console.info("second page: " + `${this.storageLink2}`) 82 } 83 84 build() { 85 Column() { 86 87 Text(`second Page ${this.storageLink2}`).fontSize(50) 88 Button('Change Divider.strokeWidth') 89 .onClick(() => { 90 router.back() 91 }) 92 93 Button('second page storageLink2 + 2').fontSize(30) 94 .onClick(() => { 95 this.storageLink2 += 2 96 }) 97 98 } 99 } 100} 101``` 102 103In the preceding example: 104 1051. When the button **first page storageLink + 1** on page 1 is clicked, the **storageLink** state variable is updated, and the @Watch decorated **first** method is called. 106 1072. Through **router.pushUrl({url:'pages/second'})**, page 2 is displayed, and page 1 is hidden with its state changing from active to inactive. 108 1093. When the button **this.storageLink2 += 2** on page 2 is clicked, only the @Watch decorated **second** method of page 2 is called, because page 1 has been frozen when inactive. 110 1114. When the **back** button is clicked, page 2 is destroyed, and page 1 changes from inactive to active. At this time, if the state variable of page 1 is updated, the @Watch decorated **first** method of page 1 is called again. 112 113 114### TabContent 115 116- You can freeze invisible **TabContent** components in the **Tabs** container so that they do not trigger UI re-rendering. 117 118- During initial rendering, only the **TabContent** component that is being displayed is created. All **TabContent** components are created only after all of them have been switched to. 119 120For details, see the following. 121 122 123```ts 124@Entry 125@Component 126struct TabContentTest { 127 @State @Watch("onMessageUpdated") message: number = 0; 128 private data: number[] = [0, 1] 129 130 onMessageUpdated() { 131 console.info(`TabContent message callback func ${this.message}`) 132 } 133 134 build() { 135 Row() { 136 Column() { 137 Button('change message').onClick(() => { 138 this.message++ 139 }) 140 141 Tabs() { 142 ForEach(this.data, (item: number) => { 143 TabContent() { 144 FreezeChild({ message: this.message, index: item }) 145 }.tabBar(`tab${item}`) 146 }, (item: number) => item.toString()) 147 } 148 } 149 .width('100%') 150 } 151 .height('100%') 152 } 153} 154 155@Component({ freezeWhenInactive: true }) 156struct FreezeChild { 157 @Link @Watch("onMessageUpdated") message: number 158 private index: number = 0 159 160 onMessageUpdated() { 161 console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`) 162 } 163 164 build() { 165 Text("message" + `${this.message}, index: ${this.index}`) 166 .fontSize(50) 167 .fontWeight(FontWeight.Bold) 168 } 169} 170``` 171 172In the preceding example: 173 1741. When **change message** is clicked, the value of **message** changes, and the @Watch decorated **onMessageUpdated** method of the **TabContent** component being displayed is called. 175 1762. When you click **two** to switch to another **TabContent** component, it switches from inactive to active, and the corresponding @Watch decorated **onMessageUpdated** method is called. 177 1783. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **onMessageUpdated** method of the **TabContent** component being displayed is called. 179 180 181 182 183### LazyForEach 184 185- You can freeze custom components cached in **LazyForEach** so that they do not trigger UI re-rendering. 186 187```ts 188// Basic implementation of IDataSource used to listening for data. 189class BasicDataSource implements IDataSource { 190 private listeners: DataChangeListener[] = []; 191 private originDataArray: string[] = []; 192 193 public totalCount(): number { 194 return 0; 195 } 196 197 public getData(index: number): string { 198 return this.originDataArray[index]; 199 } 200 201 // This method is called by the framework to add a listener to the LazyForEach data source. 202 registerDataChangeListener(listener: DataChangeListener): void { 203 if (this.listeners.indexOf(listener) < 0) { 204 console.info('add listener'); 205 this.listeners.push(listener); 206 } 207 } 208 209 // This method is called by the framework to remove the listener from the LazyForEach data source. 210 unregisterDataChangeListener(listener: DataChangeListener): void { 211 const pos = this.listeners.indexOf(listener); 212 if (pos >= 0) { 213 console.info('remove listener'); 214 this.listeners.splice(pos, 1); 215 } 216 } 217 218 // Notify LazyForEach that all child components need to be reloaded. 219 notifyDataReload(): void { 220 this.listeners.forEach(listener => { 221 listener.onDataReloaded(); 222 }) 223 } 224 225 // Notify LazyForEach that a child component needs to be added for the data item with the specified index. 226 notifyDataAdd(index: number): void { 227 this.listeners.forEach(listener => { 228 listener.onDataAdd(index); 229 }) 230 } 231 232 // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt. 233 notifyDataChange(index: number): void { 234 this.listeners.forEach(listener => { 235 listener.onDataChange(index); 236 }) 237 } 238 239 // Notify LazyForEach that the child component that matches the specified index needs to be deleted. 240 notifyDataDelete(index: number): void { 241 this.listeners.forEach(listener => { 242 listener.onDataDelete(index); 243 }) 244 } 245} 246 247class MyDataSource extends BasicDataSource { 248 private dataArray: string[] = []; 249 250 public totalCount(): number { 251 return this.dataArray.length; 252 } 253 254 public getData(index: number): string { 255 return this.dataArray[index]; 256 } 257 258 public addData(index: number, data: string): void { 259 this.dataArray.splice(index, 0, data); 260 this.notifyDataAdd(index); 261 } 262 263 public pushData(data: string): void { 264 this.dataArray.push(data); 265 this.notifyDataAdd(this.dataArray.length - 1); 266 } 267} 268 269@Entry 270@Component 271struct LforEachTest { 272 private data: MyDataSource = new MyDataSource(); 273 @State @Watch("onMessageUpdated") message: number = 0; 274 275 onMessageUpdated() { 276 console.info(`LazyforEach message callback func ${this.message}`) 277 } 278 279 aboutToAppear() { 280 for (let i = 0; i <= 20; i++) { 281 this.data.pushData(`Hello ${i}`) 282 } 283 } 284 285 build() { 286 Column() { 287 Button('change message').onClick(() => { 288 this.message++ 289 }) 290 List({ space: 3 }) { 291 LazyForEach(this.data, (item: string) => { 292 ListItem() { 293 FreezeChild({ message: this.message, index: item }) 294 } 295 }, (item: string) => item) 296 }.cachedCount(5).height(500) 297 } 298 299 } 300} 301 302@Component({ freezeWhenInactive: true }) 303struct FreezeChild { 304 @Link @Watch("onMessageUpdated") message: number; 305 private index: string = ""; 306 307 aboutToAppear() { 308 console.info(`FreezeChild aboutToAppear index: ${this.index}`) 309 } 310 311 onMessageUpdated() { 312 console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`) 313 } 314 315 build() { 316 Text("message" + `${this.message}, index: ${this.index}`) 317 .width('90%') 318 .height(160) 319 .backgroundColor(0xAFEEEE) 320 .textAlign(TextAlign.Center) 321 .fontSize(30) 322 .fontWeight(FontWeight.Bold) 323 } 324} 325``` 326 327In the preceding example: 328 3291. When **change message** is clicked, the value of **message** changes, the @Watch decorated **onMessageUpdated** method of the list items being displayed is called, and that of the cached list items is not called. (If the component is not frozen, the @Watch decorated **onMessageUpdated** method of both list items that are being displayed and cached list items is called.) 330 3312. When a list item moves from outside the list content area into the list content area, it switches from inactive to active, and the corresponding @Watch decorated **onMessageUpdated** method is called. 332 3333. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **onMessageUpdated** method of the list items being displayed is called. 334 335 336 337### Navigation 338 339- When the navigation destination page is invisible, its child custom components are set to the inactive state and will not be re-rendered. When return to this page, its child custom components are restored to the active state and the @Watch callback is triggered to re-render the page. 340 341- In the following example, **NavigationContentMsgStack** is set to the inactive state, which does not respond to the change of the state variables, and does not trigger component re-rendering. 342 343```ts 344@Entry 345@Component 346struct MyNavigationTestStack { 347 @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 348 @State @Watch("info") message: number = 0; 349 @State logNumber: number = 0; 350 351 info() { 352 console.info(`freeze-test MyNavigation message callback ${this.message}`); 353 } 354 355 @Builder 356 PageMap(name: string) { 357 if (name === 'pageOne') { 358 pageOneStack({ message: this.message, logNumber: this.logNumber }) 359 } else if (name === 'pageTwo') { 360 pageTwoStack({ message: this.message, logNumber: this.logNumber }) 361 } else if (name === 'pageThree') { 362 pageThreeStack({ message: this.message, logNumber: this.logNumber }) 363 } 364 } 365 366 build() { 367 Column() { 368 Button('change message') 369 .onClick(() => { 370 this.message++; 371 }) 372 Navigation(this.pageInfo) { 373 Column() { 374 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 375 .width('80%') 376 .height(40) 377 .margin(20) 378 .onClick(() => { 379 this.pageInfo.pushPath({ name: 'pageOne' }); // Push the navigation destination page specified by name to the navigation stack. 380 }) 381 } 382 }.title('NavIndex') 383 .navDestination(this.PageMap) 384 .mode(NavigationMode.Stack) 385 } 386 } 387} 388 389@Component 390struct pageOneStack { 391 @Consume('pageInfo') pageInfo: NavPathStack; 392 @State index: number = 1; 393 @Link message: number; 394 @Link logNumber: number; 395 396 build() { 397 NavDestination() { 398 Column() { 399 NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber }) 400 Text("cur stack size:" + `${this.pageInfo.size()}`) 401 .fontSize(30) 402 .fontWeight(FontWeight.Bold) 403 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 404 .width('80%') 405 .height(40) 406 .margin(20) 407 .onClick(() => { 408 this.pageInfo.pushPathByName('pageTwo', null); 409 }) 410 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 411 .width('80%') 412 .height(40) 413 .margin(20) 414 .onClick(() => { 415 this.pageInfo.pop(); 416 }) 417 }.width('100%').height('100%') 418 }.title('pageOne') 419 .onBackPressed(() => { 420 this.pageInfo.pop(); 421 return true; 422 }) 423 } 424} 425 426@Component 427struct pageTwoStack { 428 @Consume('pageInfo') pageInfo: NavPathStack; 429 @State index: number = 2; 430 @Link message: number; 431 @Link logNumber: number; 432 433 build() { 434 NavDestination() { 435 Column() { 436 NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber }) 437 Text("cur stack size:" + `${this.pageInfo.size()}`) 438 .fontSize(30) 439 .fontWeight(FontWeight.Bold) 440 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 441 .width('80%') 442 .height(40) 443 .margin(20) 444 .onClick(() => { 445 this.pageInfo.pushPathByName('pageThree', null); 446 }) 447 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 448 .width('80%') 449 .height(40) 450 .margin(20) 451 .onClick(() => { 452 this.pageInfo.pop(); 453 }) 454 }.width('100%').height('100%') 455 }.title('pageTwo') 456 .onBackPressed(() => { 457 this.pageInfo.pop(); 458 return true; 459 }) 460 } 461} 462 463@Component 464struct pageThreeStack { 465 @Consume('pageInfo') pageInfo: NavPathStack; 466 @State index: number = 3; 467 @Link message: number; 468 @Link logNumber: number; 469 470 build() { 471 NavDestination() { 472 Column() { 473 NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber }) 474 Text("cur stack size:" + `${this.pageInfo.size()}`) 475 .fontSize(30) 476 .fontWeight(FontWeight.Bold) 477 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 478 .width('80%') 479 .height(40) 480 .margin(20) 481 .onClick(() => { 482 this.pageInfo.pushPathByName('pageOne', null); 483 }) 484 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 485 .width('80%') 486 .height(40) 487 .margin(20) 488 .onClick(() => { 489 this.pageInfo.pop(); 490 }) 491 }.width('100%').height('100%') 492 }.title('pageThree') 493 .onBackPressed(() => { 494 this.pageInfo.pop(); 495 return true; 496 }) 497 } 498} 499 500@Component({ freezeWhenInactive: true }) 501struct NavigationContentMsgStack { 502 @Link @Watch("info") message: number; 503 @Link index: number; 504 @Link logNumber: number; 505 506 info() { 507 console.info(`freeze-test NavigationContent message callback ${this.message}`); 508 console.info(`freeze-test ---- called by content ${this.index}`); 509 this.logNumber++; 510 } 511 512 build() { 513 Column() { 514 Text("msg:" + `${this.message}`) 515 .fontSize(30) 516 .fontWeight(FontWeight.Bold) 517 Text("log number:" + `${this.logNumber}`) 518 .fontSize(30) 519 .fontWeight(FontWeight.Bold) 520 } 521 } 522} 523``` 524 525In the preceding example: 526 5271. When **change message** is clicked, the value of **message** changes, and the @Watch decorated **info** method of the **MyNavigationTestStack** component being displayed is called. 528 5292. When **Next Page** is clicked, **PageOne** is displayed, and the **PageOneStack** node is created. 530 5313. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageOneStack** is called. 532 5334. When **Next Page** is clicked again, **PageTwo** is displayed, and the **pageTwoStack** node is created. 534 5355. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageTwoStack** is called. 536 5376. When **Next Page** is clicked again, **PageThree** is displayed, and the **pageThreeStack** node is created. 538 5397. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageThreeStack** is called. 540 5418. When **Back Page** is clicked, **PageTwo** is displayed, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageTwoStack** is called. 542 5439. When **Back Page** is clicked again, **PageOne** is displayed, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageOneStack** is called. 544 54510. When **Back Page** is clicked again, the initial page is displayed, and no method is called. 546 547 548 549### Reusing Components 550 551<!--RP1-->[Components reuse](../performance/component-recycle.md)<!--RP1End--> existing nodes in the cache pool instead of creating new nodes to optimize UI performance and improve application smoothness. Although the nodes in the reuse pool are not displayed in the UI component tree, the change of the state variable still triggers the UI re-render. To solve the problem that components in the reuse pool are re-rendered abnormally, you can perform component freezing. 552 553#### Mixed Use of Component Reuse, if, and Component Freezing 554The following example shows that when the state variable bound to the **if** component changes to **false**, the detach of **ChildComponent** is triggered. Because **ChildComponent** is marked as component reuse, it is not destroyed but enters the reuse pool, in this case, if the component freezing is enabled at the same time, the component will not be re-rendered in the reuse pool. 555The procedure is as follows: 5561. Click **change flag** and change the value of **flag** to **false**. 557 - When **ChildComponent** marked with \@Reusable is detached, it is not destroyed. Instead, it enters the reuse pool, triggers the **aboutToRecycle** lifecycle, and sets the component state to inactive. 558 - **ChildComponent** also enables component freezing. When **ChildComponent** is in the inactive state, it does not respond to any UI re-render caused by state variable changes. 5592. Click **change desc** to trigger the change of the member variable **desc** of **Page**. 560 - The change of \@State decorated **desc** will be notified to \@Link decorated **desc** of **ChildComponent**. 561 - However, **ChildComponent** is in the inactive state and the component freezing is enabled. Therefore, the change does not trigger the callback of @Watch('descChange') and the re-render of the `ChildComponent` UI. If component freezing is not enabled, the current @Watch('descChange') callback is returned immediately, and **ChildComponent** in the reuse pool is re-rendered accordingly. 5623. Click **change flag** again and change the value of **flag** to **true**. 563 - **ChildComponent** is attached to the component tree from the reuse pool. 564 - Return the **aboutToReuse** lifecycle callback and synchronize the latest **count** value to **ChildComponent**. The value of **desc** is synchronized from @State to @Link. Therefore, you do not need to manually assign a value to **aboutToReuse**. 565 - Set **ChildComponent** to the active state and re-render the component that is not re-rendered when **ChildComponent** is inactive, for example, **Text (ChildComponent desc: ${this.desc})**. 566 567 568```ts 569@Reusable 570@Component({freezeWhenInactive: true}) 571struct ChildComponent { 572 @Link @Watch('descChange') desc: string; 573 @State count: number = 0; 574 descChange() { 575 console.info(`ChildComponent messageChange ${this.desc}`); 576 } 577 578 aboutToReuse(params: Record<string, ESObject>): void { 579 this.count = params.count as number; 580 } 581 582 aboutToRecycle(): void { 583 console.info(`ChildComponent has been recycled`); 584 } 585 build() { 586 Column() { 587 Text(`ChildComponent desc: ${this.desc}`) 588 .fontSize(20) 589 Text(`ChildComponent count ${this.count}`) 590 .fontSize(20) 591 }.border({width: 2, color: Color.Pink}) 592 } 593} 594 595@Entry 596@Component 597struct Page { 598 @State desc: string = 'Hello World'; 599 @State flag: boolean = true; 600 @State count: number = 0; 601 build() { 602 Column() { 603 Button(`change desc`).onClick(() => { 604 this.desc += '!'; 605 }) 606 Button(`change flag`).onClick(() => { 607 this.count++; 608 this.flag =! this.flag; 609 }) 610 if (this.flag) { 611 ChildComponent({desc: this.desc, count: this.count}) 612 } 613 } 614 .height('100%') 615 } 616} 617``` 618#### Mixed Use of LazyForEach, Component Reuse, and Component Freezing 619In the scrolling scenario of a long list with a large amount of data, you can use **LazyForEach** to create components as required. In addition, you can reuse components to reduce the overhead caused by component creation and destruction during scrolling. 620However, if you set <!--RP2-->[reuseId](../performance/component-recycle.md#available-apis)<!--RP2End--> based on the reuse type or assign a large value to **cacheCount** to ensure the scrolling performance, more nodes will be cached in the reuse pool or **LazyForEach**. 621In this case, if you trigger the re-render of all subnodes in **List**, the number of re-renders is too large. In this case, you can freeze the component. 622 623Example: 6241. Swipe the list to the position whose index is 14. There are 15 **ChildComponent** in the visible area on the current page. 6252. During swiping: 626 - **ChildComponent** in the upper part of the list is swiped out of the visible area. In this case, **ChildComponent** enters the cache area of LazyForEach and is set to inactive. After the component slides out of the **LazyForEach** area, the component is not destructed and enters the reuse pool because the component is marked for reuse. In this case, the component is set to inactive again. 627 - The cache node of **LazyForEach** at the bottom of the list enters the list. In this case, the system attempts to create a node to enter the cache of **LazyForEach**. If a node that can be reused is found, the system takes out the existing node from the reuse pool and triggers the **aboutToReuse** lifecycle callback, in this case, the node enters the cache area of **LazyForEach** and the state of the node is still inactive. 6283. Click **change desc** to trigger the change of the member variable **desc** of **Page**. 629 - The change of \@State decorated **desc** will be notified to \@Link decorated **desc** of **ChildComponent**. 630 - **ChildComponent** in the invisible area is in the inactive state, and the component freezing is enabled. Therefore, this change triggers the @Watch('descChange') callback of the 15 nodes in the visible area and re-renders these nodes. Nodes cached in **LazyForEach** and the reuse pool are not re-rendered, and the \@Watch callback is not triggered. 631 632 633For details, see the following. 634 635You can listen for the changes by \@Trace, only 15 **ChildComponent** nodes are re-rendered. 636 637A complete sample code is as follows: 638```ts 639import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'; 640// Basic implementation of IDataSource used to listening for data. 641class BasicDataSource implements IDataSource { 642 private listeners: DataChangeListener[] = []; 643 private originDataArray: string[] = []; 644 645 public totalCount(): number { 646 return 0; 647 } 648 649 public getData(index: number): string { 650 return this.originDataArray[index]; 651 } 652 653 // This method is called by the framework to add a listener to the LazyForEach data source. 654 registerDataChangeListener(listener: DataChangeListener): void { 655 if (this.listeners.indexOf(listener) < 0) { 656 console.info('add listener'); 657 this.listeners.push(listener); 658 } 659 } 660 661 // This method is called by the framework to remove the listener from the LazyForEach data source. 662 unregisterDataChangeListener(listener: DataChangeListener): void { 663 const pos = this.listeners.indexOf(listener); 664 if (pos >= 0) { 665 console.info('remove listener'); 666 this.listeners.splice(pos, 1); 667 } 668 } 669 670 // Notify LazyForEach that all child components need to be reloaded. 671 notifyDataReload(): void { 672 this.listeners.forEach(listener => { 673 listener.onDataReloaded(); 674 }) 675 } 676 677 // Notify LazyForEach that a child component needs to be added for the data item with the specified index. 678 notifyDataAdd(index: number): void { 679 this.listeners.forEach(listener => { 680 listener.onDataAdd(index); 681 }) 682 } 683 684 // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt. 685 notifyDataChange(index: number): void { 686 this.listeners.forEach(listener => { 687 listener.onDataChange(index); 688 }) 689 } 690 691 // Notify LazyForEach that the child component that matches the specified index needs to be deleted. 692 notifyDataDelete(index: number): void { 693 this.listeners.forEach(listener => { 694 listener.onDataDelete(index); 695 }) 696 } 697 698 // Notify LazyForEach that data needs to be swapped between the from and to positions. 699 notifyDataMove(from: number, to: number): void { 700 this.listeners.forEach(listener => { 701 listener.onDataMove(from, to); 702 }) 703 } 704} 705 706class MyDataSource extends BasicDataSource { 707 private dataArray: string[] = []; 708 709 public totalCount(): number { 710 return this.dataArray.length; 711 } 712 713 public getData(index: number): string { 714 return this.dataArray[index]; 715 } 716 717 public addData(index: number, data: string): void { 718 this.dataArray.splice(index, 0, data); 719 this.notifyDataAdd(index); 720 } 721 722 public pushData(data: string): void { 723 this.dataArray.push(data); 724 this.notifyDataAdd(this.dataArray.length - 1); 725 } 726} 727 728@Reusable 729@Component({freezeWhenInactive: true}) 730struct ChildComponent { 731 @Link @Watch('descChange') desc: string; 732 @State item: string = ''; 733 @State index: number = 0; 734 descChange() { 735 console.info(`ChildComponent messageChange ${this.desc}`); 736 } 737 738 aboutToReuse(params: Record<string, ESObject>): void { 739 this.item = params.item; 740 this.index = params.index; 741 } 742 743 aboutToRecycle(): void { 744 console.info(`ChildComponent has been recycled`); 745 } 746 build() { 747 Column() { 748 Text(`ChildComponent index: ${this.index} item: ${this.item}`) 749 .fontSize(20) 750 Text(`desc: ${this.desc}`) 751 .fontSize(20) 752 }.border({width: 2, color: Color.Pink}) 753 } 754} 755 756@Entry 757@Component 758struct Page { 759 @State desc: string = 'Hello World'; 760 private data: MyDataSource = new MyDataSource(); 761 762 aboutToAppear() { 763 for (let i = 0; i < 50; i++) { 764 this.data.pushData(`Hello ${i}`); 765 } 766 } 767 768 build() { 769 Column() { 770 Button(`change desc`).onClick(() => { 771 hiTraceMeter.startTrace('change decs', 1); 772 this.desc += '!'; 773 hiTraceMeter.finishTrace('change decs', 1); 774 }) 775 List({ space: 3 }) { 776 LazyForEach(this.data, (item: string, index: number) => { 777 ListItem() { 778 ChildComponent({index: index, item: item, desc: this.desc}).reuseId(index % 10 < 5 ? "1": "0") 779 } 780 }, (item: string) => item) 781 }.cachedCount(5) 782 } 783 .height('100%') 784 } 785} 786``` 787#### Mixed Use of LazyForEach, if, Component Reuse, and Component Freezing 788 789 Under the same parent custom component, reusable nodes may enter the reuse pool in different ways. For example: 790- Detaching from the cache area of LazyForEach by swiping. 791- Notifying the subnodes to detach by switching the if condition. 792 793In the following example: 7941. When you swipe the list to the position whose index is 14, there are 10 **ChildComponent**s in the visible area on the page, among which nine are subnodes of **LazyForEach** and one is a subnode of **if**. 7952. Click **change flag**. The **if** condition is changed to **false**, and its subnode **ChildComponent** enters the reuse pool. Nine nodes are displayed on the page. 7963. In this case, the nodes detached through **LazyForEach** or **if** all enter the reuse pool under the **Page** node. 7974. Click **change desc** to update only the nine **ChildComponent** nodes on the page. For details, see figures below. 7985. Click **change flag** again. The **if** condition changes to **true**, and **ChildComponent** is attached from the reuse pool to the component tree again. The state of **ChildComponent** changes to active. 7996. Click **change desc** again. The nodes attached through **if** and **LazyForEach** from the reuse pool can be re-rendered. 800 801Trace for component freezing enabled 802 803 804 805Trace for component freezing disabled 806 807 808 809 810A complete example is as follows: 811``` 812import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'; 813class BasicDataSource implements IDataSource { 814 private listeners: DataChangeListener[] = []; 815 private originDataArray: string[] = []; 816 817 public totalCount(): number { 818 return 0; 819 } 820 821 public getData(index: number): string { 822 return this.originDataArray[index]; 823 } 824 825 // This method is called by the framework to add a listener to the LazyForEach data source. 826 registerDataChangeListener(listener: DataChangeListener): void { 827 if (this.listeners.indexOf(listener) < 0) { 828 console.info('add listener'); 829 this.listeners.push(listener); 830 } 831 } 832 833 // This method is called by the framework to remove the listener from the LazyForEach data source. 834 unregisterDataChangeListener(listener: DataChangeListener): void { 835 const pos = this.listeners.indexOf(listener); 836 if (pos >= 0) { 837 console.info('remove listener'); 838 this.listeners.splice(pos, 1); 839 } 840 } 841 842 // Notify LazyForEach that all child components need to be reloaded. 843 notifyDataReload(): void { 844 this.listeners.forEach(listener => { 845 listener.onDataReloaded(); 846 }) 847 } 848 849 // Notify LazyForEach that a child component needs to be added for the data item with the specified index. 850 notifyDataAdd(index: number): void { 851 this.listeners.forEach(listener => { 852 listener.onDataAdd(index); 853 }) 854 } 855 856 // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt. 857 notifyDataChange(index: number): void { 858 this.listeners.forEach(listener => { 859 listener.onDataChange(index); 860 }) 861 } 862 863 // Notify LazyForEach that the child component that matches the specified index needs to be deleted. 864 notifyDataDelete(index: number): void { 865 this.listeners.forEach(listener => { 866 listener.onDataDelete(index); 867 }) 868 } 869 870 // Notify LazyForEach that data needs to be swapped between the from and to positions. 871 notifyDataMove(from: number, to: number): void { 872 this.listeners.forEach(listener => { 873 listener.onDataMove(from, to); 874 }) 875 } 876} 877 878class MyDataSource extends BasicDataSource { 879 private dataArray: string[] = []; 880 881 public totalCount(): number { 882 return this.dataArray.length; 883 } 884 885 public getData(index: number): string { 886 return this.dataArray[index]; 887 } 888 889 public addData(index: number, data: string): void { 890 this.dataArray.splice(index, 0, data); 891 this.notifyDataAdd(index); 892 } 893 894 public pushData(data: string): void { 895 this.dataArray.push(data); 896 this.notifyDataAdd(this.dataArray.length - 1); 897 } 898} 899 900@Reusable 901@Component({freezeWhenInactive: true}) 902struct ChildComponent { 903 @Link @Watch('descChange') desc: string; 904 @State item: string = ''; 905 @State index: number = 0; 906 descChange() { 907 console.info(`ChildComponent messageChange ${this.desc}`); 908 } 909 910 aboutToReuse(params: Record<string, ESObject>): void { 911 this.item = params.item; 912 this.index = params.index; 913 } 914 915 aboutToRecycle(): void { 916 console.info(`ChildComponent has been recycled`); 917 } 918 build() { 919 Column() { 920 Text(`ChildComponent index: ${this.index} item: ${this.item}`) 921 .fontSize(20) 922 Text(`desc: ${this.desc}`) 923 .fontSize(20) 924 }.border({width: 2, color: Color.Pink}) 925 } 926} 927 928@Entry 929@Component 930struct Page { 931 @State desc: string = 'Hello World'; 932 @State flag: boolean = true; 933 private data: MyDataSource = new MyDataSource(); 934 935 aboutToAppear() { 936 for (let i = 0; i < 50; i++) { 937 this.data.pushData(`Hello ${i}`); 938 } 939 } 940 941 build() { 942 Column() { 943 Button(`change desc`).onClick(() => { 944 hiTraceMeter.startTrace('change decs', 1); 945 this.desc += '!'; 946 hiTraceMeter.finishTrace('change decs', 1); 947 }) 948 949 Button(`change flag`).onClick(() => { 950 hiTraceMeter.startTrace('change flag', 1); 951 this.flag = !this.flag; 952 hiTraceMeter.finishTrace('change flag', 1); 953 }) 954 955 List({ space: 3 }) { 956 LazyForEach(this.data, (item: string, index: number) => { 957 ListItem() { 958 ChildComponent({index: index, item: item, desc: this.desc}).reuseId(index % 10 < 5 ? "1": "0") 959 } 960 }, (item: string) => item) 961 } 962 .cachedCount(5) 963 .height('60%') 964 965 if (this.flag) { 966 ChildComponent({index: -1, item: 'Hello', desc: this.desc}).reuseId( "1") 967 } 968 } 969 .height('100%') 970 } 971} 972``` 973 974## Constraints 975 976As shown in the following example, the custom node [BuilderNode](../reference/apis-arkui/js-apis-arkui-builderNode.md) is used in **FreezeBuildNode**. **BuilderNode** can dynamically mount components using commands and component freezing strongly depends on the parent-child relationship to determine whether it is enabled. In this case, if the parent component is frozen and **BuilderNode** is enabled at the middle level of the component tree, the child component of the **BuilderNode** cannot be frozen. 977 978``` 979import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI'; 980 981// Define a Params class to pass parameters. 982class Params { 983 index: number = 0; 984 985 constructor(index: number) { 986 this.index = index; 987 } 988} 989 990// Define a buildNodeChild component that contains a message attribute and an index attribute. 991@Component 992struct buildNodeChild { 993 @StorageProp("buildNodeTest") @Watch("onMessageUpdated") message: string = "hello world"; 994 @State index: number = 0; 995 996 // Call this method when message is updated. 997 onMessageUpdated() { 998 console.log(`FreezeBuildNode builderNodeChild message callback func ${this.message},index: ${this.index}`); 999 } 1000 1001 build() { 1002 Text(`buildNode Child message: ${this.message}`).fontSize(30) 1003 } 1004} 1005 1006// Define a buildText function that receives a Params parameter and constructs a Column component. 1007@Builder 1008function buildText(params: Params) { 1009 Column() { 1010 buildNodeChild({ index: params.index }) 1011 } 1012} 1013 1014// Define a TextNodeController class that is inherited from NodeController. 1015class TextNodeController extends NodeController { 1016 private textNode: BuilderNode<[Params]> | null = null; 1017 private index: number = 0; 1018 1019 // The constructor receives an index parameter. 1020 constructor(index: number) { 1021 super(); 1022 this.index = index; 1023 } 1024 1025 // Create and return a FrameNode. 1026 makeNode(context: UIContext): FrameNode | null { 1027 this.textNode = new BuilderNode(context); 1028 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.index)); 1029 return this.textNode.getFrameNode(); 1030 } 1031} 1032 1033// Define an index component that contains a message attribute and a data array. 1034@Entry 1035@Component 1036struct Index { 1037 @StorageLink("buildNodeTest") message: string = "hello"; 1038 private data: number[] = [0, 1]; 1039 1040 build() { 1041 Row() { 1042 Column() { 1043 Button("change").fontSize(30) 1044 .onClick(() => { 1045 this.message += 'a'; 1046 }) 1047 1048 Tabs() { 1049 ForEach(this.data, (item: number) => { 1050 TabContent() { 1051 FreezeBuildNode({ index: item }) 1052 }.tabBar(`tab${item}`) 1053 }, (item: number) => item.toString()) 1054 } 1055 } 1056 } 1057 .width('100%') 1058 .height('100%') 1059 } 1060} 1061 1062// Define a FreezeBuildNode component that contains a message attribute and an index attribute. 1063@Component({ freezeWhenInactive: true }) 1064struct FreezeBuildNode { 1065 @StorageProp("buildNodeTest") @Watch("onMessageUpdated") message: string = "1111"; 1066 @State index: number = 0; 1067 1068 // Call this method when message is updated. 1069 onMessageUpdated() { 1070 console.log(`FreezeBuildNode message callback func ${this.message}, index: ${this.index}`); 1071 } 1072 1073 build() { 1074 NodeContainer(new TextNodeController(this.index)) 1075 .width('100%') 1076 .height('100%') 1077 .backgroundColor('#FFF0F0F0') 1078 } 1079} 1080``` 1081 1082In the preceding example: 1083 1084Click **Button("change")** to change the value of **message**. The **onMessageUpdated** method registered in @Watch of the **TabContent** component that is being displayed is triggered, and that under the **BuilderNode** node of **TabContent** that is not displayed is also triggered. 1085 1086 1087