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. 19 20 21 22Before reading this topic, you are advised to read [Creating a Custom Component](./arkts-create-custom-components.md) to learn about the basic syntax. 23 24> **NOTE** 25> 26> Custom component freezing is supported since API version 11. 27 28## Use Scenarios 29 30### Page Routing 31 32> **NOTE** 33> 34> 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). 35 36When 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. 37For details, see the following. 38 39 40 41Page 1 42 43```ts 44import { router } from '@kit.ArkUI'; 45 46@Entry 47@Component({ freezeWhenInactive: true }) 48struct Page1 { 49 @StorageLink('PropA') @Watch("first") storageLink: number = 47; 50 51 first() { 52 console.info("first page " + `${this.storageLink}`) 53 } 54 55 build() { 56 Column() { 57 Text(`From first Page ${this.storageLink}`).fontSize(50) 58 Button('first page storageLink + 1').fontSize(30) 59 .onClick(() => { 60 this.storageLink += 1 61 }) 62 Button('go to next page').fontSize(30) 63 .onClick(() => { 64 router.pushUrl({ url: 'pages/Page2' }) 65 }) 66 } 67 } 68} 69``` 70 71Page 2 72 73```ts 74import { router } from '@kit.ArkUI'; 75 76@Entry 77@Component({ freezeWhenInactive: true }) 78struct Page2 { 79 @StorageLink('PropA') @Watch("second") storageLink2: number = 1; 80 81 second() { 82 console.info("second page: " + `${this.storageLink2}`) 83 } 84 85 build() { 86 Column() { 87 88 Text(`second Page ${this.storageLink2}`).fontSize(50) 89 Button('Change Divider.strokeWidth') 90 .onClick(() => { 91 router.back() 92 }) 93 94 Button('second page storageLink2 + 2').fontSize(30) 95 .onClick(() => { 96 this.storageLink2 += 2 97 }) 98 99 } 100 } 101} 102``` 103 104In the preceding example: 105 1061. 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. 107 1082. Through **router.pushUrl({url:'pages/second'})**, page 2 is displayed, and page 1 is hidden with its state changing from active to inactive. 109 1103. 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. 111 1124. 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. 113 114 115### TabContent 116 117- You can freeze invisible **TabContent** components in the **Tabs** container so that they do not trigger UI re-rendering. 118 119- 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. 120 121For details, see the following. 122 123 124```ts 125@Entry 126@Component 127struct TabContentTest { 128 @State @Watch("onMessageUpdated") message: number = 0; 129 private data: number[] = [0, 1] 130 131 onMessageUpdated() { 132 console.info(`TabContent message callback func ${this.message}`) 133 } 134 135 build() { 136 Row() { 137 Column() { 138 Button('change message').onClick(() => { 139 this.message++ 140 }) 141 142 Tabs() { 143 ForEach(this.data, (item: number) => { 144 TabContent() { 145 FreezeChild({ message: this.message, index: item }) 146 }.tabBar(`tab${item}`) 147 }, (item: number) => item.toString()) 148 } 149 } 150 .width('100%') 151 } 152 .height('100%') 153 } 154} 155 156@Component({ freezeWhenInactive: true }) 157struct FreezeChild { 158 @Link @Watch("onMessageUpdated") message: number 159 private index: number = 0 160 161 onMessageUpdated() { 162 console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`) 163 } 164 165 build() { 166 Text("message" + `${this.message}, index: ${this.index}`) 167 .fontSize(50) 168 .fontWeight(FontWeight.Bold) 169 } 170} 171``` 172 173In the preceding example: 174 1751. When **change message** is clicked, the value of **message** changes, and the @Watch decorated **onMessageUpdated** method of the **TabContent** component being displayed is called. 176 1772. 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. 178 1793. 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. 180 181 182 183 184### LazyForEach 185 186- You can freeze custom components cached in **LazyForEach** so that they do not trigger UI re-rendering. 187 188```ts 189// Basic implementation of IDataSource used to listening for data. 190class BasicDataSource implements IDataSource { 191 private listeners: DataChangeListener[] = []; 192 private originDataArray: string[] = []; 193 194 public totalCount(): number { 195 return 0; 196 } 197 198 public getData(index: number): string { 199 return this.originDataArray[index]; 200 } 201 202 // This method is called by the framework to add a listener to the LazyForEach data source. 203 registerDataChangeListener(listener: DataChangeListener): void { 204 if (this.listeners.indexOf(listener) < 0) { 205 console.info('add listener'); 206 this.listeners.push(listener); 207 } 208 } 209 210 // This method is called by the framework to remove the listener from the LazyForEach data source. 211 unregisterDataChangeListener(listener: DataChangeListener): void { 212 const pos = this.listeners.indexOf(listener); 213 if (pos >= 0) { 214 console.info('remove listener'); 215 this.listeners.splice(pos, 1); 216 } 217 } 218 219 // Notify LazyForEach that all child components need to be reloaded. 220 notifyDataReload(): void { 221 this.listeners.forEach(listener => { 222 listener.onDataReloaded(); 223 }) 224 } 225 226 // Notify LazyForEach that a child component needs to be added for the data item with the specified index. 227 notifyDataAdd(index: number): void { 228 this.listeners.forEach(listener => { 229 listener.onDataAdd(index); 230 }) 231 } 232 233 // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt. 234 notifyDataChange(index: number): void { 235 this.listeners.forEach(listener => { 236 listener.onDataChange(index); 237 }) 238 } 239 240 // Notify LazyForEach that the child component that matches the specified index needs to be deleted. 241 notifyDataDelete(index: number): void { 242 this.listeners.forEach(listener => { 243 listener.onDataDelete(index); 244 }) 245 } 246} 247 248class MyDataSource extends BasicDataSource { 249 private dataArray: string[] = []; 250 251 public totalCount(): number { 252 return this.dataArray.length; 253 } 254 255 public getData(index: number): string { 256 return this.dataArray[index]; 257 } 258 259 public addData(index: number, data: string): void { 260 this.dataArray.splice(index, 0, data); 261 this.notifyDataAdd(index); 262 } 263 264 public pushData(data: string): void { 265 this.dataArray.push(data); 266 this.notifyDataAdd(this.dataArray.length - 1); 267 } 268} 269 270@Entry 271@Component 272struct LforEachTest { 273 private data: MyDataSource = new MyDataSource(); 274 @State @Watch("onMessageUpdated") message: number = 0; 275 276 onMessageUpdated() { 277 console.info(`LazyforEach message callback func ${this.message}`) 278 } 279 280 aboutToAppear() { 281 for (let i = 0; i <= 20; i++) { 282 this.data.pushData(`Hello ${i}`) 283 } 284 } 285 286 build() { 287 Column() { 288 Button('change message').onClick(() => { 289 this.message++ 290 }) 291 List({ space: 3 }) { 292 LazyForEach(this.data, (item: string) => { 293 ListItem() { 294 FreezeChild({ message: this.message, index: item }) 295 } 296 }, (item: string) => item) 297 }.cachedCount(5).height(500) 298 } 299 300 } 301} 302 303@Component({ freezeWhenInactive: true }) 304struct FreezeChild { 305 @Link @Watch("onMessageUpdated") message: number; 306 private index: string = ""; 307 308 aboutToAppear() { 309 console.info(`FreezeChild aboutToAppear index: ${this.index}`) 310 } 311 312 onMessageUpdated() { 313 console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`) 314 } 315 316 build() { 317 Text("message" + `${this.message}, index: ${this.index}`) 318 .width('90%') 319 .height(160) 320 .backgroundColor(0xAFEEEE) 321 .textAlign(TextAlign.Center) 322 .fontSize(30) 323 .fontWeight(FontWeight.Bold) 324 } 325} 326``` 327 328In the preceding example: 329 3301. 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.) 331 3322. 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. 333 3343. 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. 335 336 337 338### Navigation 339 340- 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. 341 342- 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. 343 344```ts 345@Entry 346@Component 347struct MyNavigationTestStack { 348 @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 349 @State @Watch("info") message: number = 0; 350 @State logNumber: number = 0; 351 352 info() { 353 console.info(`freeze-test MyNavigation message callback ${this.message}`); 354 } 355 356 @Builder 357 PageMap(name: string) { 358 if (name === 'pageOne') { 359 pageOneStack({ message: this.message, logNumber: this.logNumber }) 360 } else if (name === 'pageTwo') { 361 pageTwoStack({ message: this.message, logNumber: this.logNumber }) 362 } else if (name === 'pageThree') { 363 pageThreeStack({ message: this.message, logNumber: this.logNumber }) 364 } 365 } 366 367 build() { 368 Column() { 369 Button('change message') 370 .onClick(() => { 371 this.message++; 372 }) 373 Navigation(this.pageInfo) { 374 Column() { 375 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 376 .width('80%') 377 .height(40) 378 .margin(20) 379 .onClick(() => { 380 this.pageInfo.pushPath({ name: 'pageOne' }); // Push the navigation destination page specified by name to the navigation stack. 381 }) 382 } 383 }.title('NavIndex') 384 .navDestination(this.PageMap) 385 .mode(NavigationMode.Stack) 386 } 387 } 388} 389 390@Component 391struct pageOneStack { 392 @Consume('pageInfo') pageInfo: NavPathStack; 393 @State index: number = 1; 394 @Link message: number; 395 @Link logNumber: number; 396 397 build() { 398 NavDestination() { 399 Column() { 400 NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber }) 401 Text("cur stack size:" + `${this.pageInfo.size()}`) 402 .fontSize(30) 403 .fontWeight(FontWeight.Bold) 404 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 405 .width('80%') 406 .height(40) 407 .margin(20) 408 .onClick(() => { 409 this.pageInfo.pushPathByName('pageTwo', null); 410 }) 411 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 412 .width('80%') 413 .height(40) 414 .margin(20) 415 .onClick(() => { 416 this.pageInfo.pop(); 417 }) 418 }.width('100%').height('100%') 419 }.title('pageOne') 420 .onBackPressed(() => { 421 this.pageInfo.pop(); 422 return true; 423 }) 424 } 425} 426 427@Component 428struct pageTwoStack { 429 @Consume('pageInfo') pageInfo: NavPathStack; 430 @State index: number = 2; 431 @Link message: number; 432 @Link logNumber: number; 433 434 build() { 435 NavDestination() { 436 Column() { 437 NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber }) 438 Text("cur stack size:" + `${this.pageInfo.size()}`) 439 .fontSize(30) 440 .fontWeight(FontWeight.Bold) 441 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 442 .width('80%') 443 .height(40) 444 .margin(20) 445 .onClick(() => { 446 this.pageInfo.pushPathByName('pageThree', null); 447 }) 448 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 449 .width('80%') 450 .height(40) 451 .margin(20) 452 .onClick(() => { 453 this.pageInfo.pop(); 454 }) 455 }.width('100%').height('100%') 456 }.title('pageTwo') 457 .onBackPressed(() => { 458 this.pageInfo.pop(); 459 return true; 460 }) 461 } 462} 463 464@Component 465struct pageThreeStack { 466 @Consume('pageInfo') pageInfo: NavPathStack; 467 @State index: number = 3; 468 @Link message: number; 469 @Link logNumber: number; 470 471 build() { 472 NavDestination() { 473 Column() { 474 NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber }) 475 Text("cur stack size:" + `${this.pageInfo.size()}`) 476 .fontSize(30) 477 .fontWeight(FontWeight.Bold) 478 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 479 .width('80%') 480 .height(40) 481 .margin(20) 482 .onClick(() => { 483 this.pageInfo.pushPathByName('pageOne', null); 484 }) 485 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 486 .width('80%') 487 .height(40) 488 .margin(20) 489 .onClick(() => { 490 this.pageInfo.pop(); 491 }) 492 }.width('100%').height('100%') 493 }.title('pageThree') 494 .onBackPressed(() => { 495 this.pageInfo.pop(); 496 return true; 497 }) 498 } 499} 500 501@Component({ freezeWhenInactive: true }) 502struct NavigationContentMsgStack { 503 @Link @Watch("info") message: number; 504 @Link index: number; 505 @Link logNumber: number; 506 507 info() { 508 console.info(`freeze-test NavigationContent message callback ${this.message}`); 509 console.info(`freeze-test ---- called by content ${this.index}`); 510 this.logNumber++; 511 } 512 513 build() { 514 Column() { 515 Text("msg:" + `${this.message}`) 516 .fontSize(30) 517 .fontWeight(FontWeight.Bold) 518 Text("log number:" + `${this.logNumber}`) 519 .fontSize(30) 520 .fontWeight(FontWeight.Bold) 521 } 522 } 523} 524``` 525 526In the preceding example: 527 5281. When **change message** is clicked, the value of **message** changes, and the @Watch decorated **info** method of the **MyNavigationTestStack** component being displayed is called. 529 5302. When **Next Page** is clicked, **PageOne** is displayed, and the **PageOneStack** node is created. 531 5323. 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. 533 5344. When **Next Page** is clicked again, **PageTwo** is displayed, and the **pageTwoStack** node is created. 535 5365. 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. 537 5386. When **Next Page** is clicked again, **PageThree** is displayed, and the **pageThreeStack** node is created. 539 5407. 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. 541 5428. When **Back Page** is clicked, **PageTwo** is displayed, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageTwoStack** is called. 543 5449. 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. 545 54610. When **Back Page** is clicked again, the initial page is displayed, and no method is called. 547 548 549 550### Reusing Components 551 552<!--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. 553 554#### Mixed Use of Component Reuse, if, and Component Freezing 555The 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. 556The procedure is as follows: 5571. Click **change flag** and change the value of **flag** to **false**. 558 - 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. 559 - **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. 5602. Click **change desc** to trigger the change of the member variable **desc** of **Page**. 561 - The change of \@State decorated **desc** will be notified to \@Link decorated **desc** of **ChildComponent**. 562 - 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. 5633. Click **change flag** again and change the value of **flag** to **true**. 564 - **ChildComponent** is attached to the component tree from the reuse pool. 565 - 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**. 566 - 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})**. 567 568 569```ts 570@Reusable 571@Component({freezeWhenInactive: true}) 572struct ChildComponent { 573 @Link @Watch('descChange') desc: string; 574 @State count: number = 0; 575 descChange() { 576 console.info(`ChildComponent messageChange ${this.desc}`); 577 } 578 579 aboutToReuse(params: Record<string, ESObject>): void { 580 this.count = params.count as number; 581 } 582 583 aboutToRecycle(): void { 584 console.info(`ChildComponent has been recycled`); 585 } 586 build() { 587 Column() { 588 Text(`ChildComponent desc: ${this.desc}`) 589 .fontSize(20) 590 Text(`ChildComponent count ${this.count}`) 591 .fontSize(20) 592 }.border({width: 2, color: Color.Pink}) 593 } 594} 595 596@Entry 597@Component 598struct Page { 599 @State desc: string = 'Hello World'; 600 @State flag: boolean = true; 601 @State count: number = 0; 602 build() { 603 Column() { 604 Button(`change desc`).onClick(() => { 605 this.desc += '!'; 606 }) 607 Button(`change flag`).onClick(() => { 608 this.count++; 609 this.flag =! this.flag; 610 }) 611 if (this.flag) { 612 ChildComponent({desc: this.desc, count: this.count}) 613 } 614 } 615 .height('100%') 616 } 617} 618``` 619#### Mixed Use of LazyForEach, Component Reuse, and Component Freezing 620In 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. 621However, 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**. 622In 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. 623 624Example: 6251. Swipe the list to the position whose index is 14. There are 15 **ChildComponent** in the visible area on the current page. 6262. During swiping: 627 - **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. 628 - 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. 6293. Click **change desc** to trigger the change of the member variable **desc** of **Page**. 630 - The change of \@State decorated **desc** will be notified to \@Link decorated **desc** of **ChildComponent**. 631 - **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. 632 633 634For details, see the following. 635 636You can listen for the changes by \@Trace, only 15 **ChildComponent** nodes are re-rendered. 637 638A complete sample code is as follows: 639```ts 640import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'; 641// Basic implementation of IDataSource used to listening for data. 642class BasicDataSource implements IDataSource { 643 private listeners: DataChangeListener[] = []; 644 private originDataArray: string[] = []; 645 646 public totalCount(): number { 647 return 0; 648 } 649 650 public getData(index: number): string { 651 return this.originDataArray[index]; 652 } 653 654 // This method is called by the framework to add a listener to the LazyForEach data source. 655 registerDataChangeListener(listener: DataChangeListener): void { 656 if (this.listeners.indexOf(listener) < 0) { 657 console.info('add listener'); 658 this.listeners.push(listener); 659 } 660 } 661 662 // This method is called by the framework to remove the listener from the LazyForEach data source. 663 unregisterDataChangeListener(listener: DataChangeListener): void { 664 const pos = this.listeners.indexOf(listener); 665 if (pos >= 0) { 666 console.info('remove listener'); 667 this.listeners.splice(pos, 1); 668 } 669 } 670 671 // Notify LazyForEach that all child components need to be reloaded. 672 notifyDataReload(): void { 673 this.listeners.forEach(listener => { 674 listener.onDataReloaded(); 675 }) 676 } 677 678 // Notify LazyForEach that a child component needs to be added for the data item with the specified index. 679 notifyDataAdd(index: number): void { 680 this.listeners.forEach(listener => { 681 listener.onDataAdd(index); 682 }) 683 } 684 685 // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt. 686 notifyDataChange(index: number): void { 687 this.listeners.forEach(listener => { 688 listener.onDataChange(index); 689 }) 690 } 691 692 // Notify LazyForEach that the child component that matches the specified index needs to be deleted. 693 notifyDataDelete(index: number): void { 694 this.listeners.forEach(listener => { 695 listener.onDataDelete(index); 696 }) 697 } 698 699 // Notify LazyForEach that data needs to be swapped between the from and to positions. 700 notifyDataMove(from: number, to: number): void { 701 this.listeners.forEach(listener => { 702 listener.onDataMove(from, to); 703 }) 704 } 705} 706 707class MyDataSource extends BasicDataSource { 708 private dataArray: string[] = []; 709 710 public totalCount(): number { 711 return this.dataArray.length; 712 } 713 714 public getData(index: number): string { 715 return this.dataArray[index]; 716 } 717 718 public addData(index: number, data: string): void { 719 this.dataArray.splice(index, 0, data); 720 this.notifyDataAdd(index); 721 } 722 723 public pushData(data: string): void { 724 this.dataArray.push(data); 725 this.notifyDataAdd(this.dataArray.length - 1); 726 } 727} 728 729@Reusable 730@Component({freezeWhenInactive: true}) 731struct ChildComponent { 732 @Link @Watch('descChange') desc: string; 733 @State item: string = ''; 734 @State index: number = 0; 735 descChange() { 736 console.info(`ChildComponent messageChange ${this.desc}`); 737 } 738 739 aboutToReuse(params: Record<string, ESObject>): void { 740 this.item = params.item; 741 this.index = params.index; 742 } 743 744 aboutToRecycle(): void { 745 console.info(`ChildComponent has been recycled`); 746 } 747 build() { 748 Column() { 749 Text(`ChildComponent index: ${this.index} item: ${this.item}`) 750 .fontSize(20) 751 Text(`desc: ${this.desc}`) 752 .fontSize(20) 753 }.border({width: 2, color: Color.Pink}) 754 } 755} 756 757@Entry 758@Component 759struct Page { 760 @State desc: string = 'Hello World'; 761 private data: MyDataSource = new MyDataSource(); 762 763 aboutToAppear() { 764 for (let i = 0; i < 50; i++) { 765 this.data.pushData(`Hello ${i}`); 766 } 767 } 768 769 build() { 770 Column() { 771 Button(`change desc`).onClick(() => { 772 hiTraceMeter.startTrace('change decs', 1); 773 this.desc += '!'; 774 hiTraceMeter.finishTrace('change decs', 1); 775 }) 776 List({ space: 3 }) { 777 LazyForEach(this.data, (item: string, index: number) => { 778 ListItem() { 779 ChildComponent({index: index, item: item, desc: this.desc}).reuseId(index % 10 < 5 ? "1": "0") 780 } 781 }, (item: string) => item) 782 }.cachedCount(5) 783 } 784 .height('100%') 785 } 786} 787``` 788#### Mixed Use of LazyForEach, if, Component Reuse, and Component Freezing 789 790Under the same parent custom component, reusable nodes may enter the reuse pool in different ways. For example: 791- Detaching from the cache area of LazyForEach by swiping. 792- Notifying the subnodes to detach by switching the if condition. 793 794In the following example: 7951. 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**. 7962. 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. 7973. In this case, the nodes detached through **LazyForEach** or **if** all enter the reuse pool under the **Page** node. 7984. Click **change desc** to update only the nine **ChildComponent** nodes on the page. For details, see figures below. 7995. 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. 8006. Click **change desc** again. The nodes attached through **if** and **LazyForEach** from the reuse pool can be re-rendered. 801 802Trace for component freezing enabled 803 804 805 806Trace for component freezing disabled 807 808 809 810 811A complete example is as follows: 812``` 813import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'; 814class BasicDataSource implements IDataSource { 815 private listeners: DataChangeListener[] = []; 816 private originDataArray: string[] = []; 817 818 public totalCount(): number { 819 return 0; 820 } 821 822 public getData(index: number): string { 823 return this.originDataArray[index]; 824 } 825 826 // This method is called by the framework to add a listener to the LazyForEach data source. 827 registerDataChangeListener(listener: DataChangeListener): void { 828 if (this.listeners.indexOf(listener) < 0) { 829 console.info('add listener'); 830 this.listeners.push(listener); 831 } 832 } 833 834 // This method is called by the framework to remove the listener from the LazyForEach data source. 835 unregisterDataChangeListener(listener: DataChangeListener): void { 836 const pos = this.listeners.indexOf(listener); 837 if (pos >= 0) { 838 console.info('remove listener'); 839 this.listeners.splice(pos, 1); 840 } 841 } 842 843 // Notify LazyForEach that all child components need to be reloaded. 844 notifyDataReload(): void { 845 this.listeners.forEach(listener => { 846 listener.onDataReloaded(); 847 }) 848 } 849 850 // Notify LazyForEach that a child component needs to be added for the data item with the specified index. 851 notifyDataAdd(index: number): void { 852 this.listeners.forEach(listener => { 853 listener.onDataAdd(index); 854 }) 855 } 856 857 // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt. 858 notifyDataChange(index: number): void { 859 this.listeners.forEach(listener => { 860 listener.onDataChange(index); 861 }) 862 } 863 864 // Notify LazyForEach that the child component that matches the specified index needs to be deleted. 865 notifyDataDelete(index: number): void { 866 this.listeners.forEach(listener => { 867 listener.onDataDelete(index); 868 }) 869 } 870 871 // Notify LazyForEach that data needs to be swapped between the from and to positions. 872 notifyDataMove(from: number, to: number): void { 873 this.listeners.forEach(listener => { 874 listener.onDataMove(from, to); 875 }) 876 } 877} 878 879class MyDataSource extends BasicDataSource { 880 private dataArray: string[] = []; 881 882 public totalCount(): number { 883 return this.dataArray.length; 884 } 885 886 public getData(index: number): string { 887 return this.dataArray[index]; 888 } 889 890 public addData(index: number, data: string): void { 891 this.dataArray.splice(index, 0, data); 892 this.notifyDataAdd(index); 893 } 894 895 public pushData(data: string): void { 896 this.dataArray.push(data); 897 this.notifyDataAdd(this.dataArray.length - 1); 898 } 899} 900 901@Reusable 902@Component({freezeWhenInactive: true}) 903struct ChildComponent { 904 @Link @Watch('descChange') desc: string; 905 @State item: string = ''; 906 @State index: number = 0; 907 descChange() { 908 console.info(`ChildComponent messageChange ${this.desc}`); 909 } 910 911 aboutToReuse(params: Record<string, ESObject>): void { 912 this.item = params.item; 913 this.index = params.index; 914 } 915 916 aboutToRecycle(): void { 917 console.info(`ChildComponent has been recycled`); 918 } 919 build() { 920 Column() { 921 Text(`ChildComponent index: ${this.index} item: ${this.item}`) 922 .fontSize(20) 923 Text(`desc: ${this.desc}`) 924 .fontSize(20) 925 }.border({width: 2, color: Color.Pink}) 926 } 927} 928 929@Entry 930@Component 931struct Page { 932 @State desc: string = 'Hello World'; 933 @State flag: boolean = true; 934 private data: MyDataSource = new MyDataSource(); 935 936 aboutToAppear() { 937 for (let i = 0; i < 50; i++) { 938 this.data.pushData(`Hello ${i}`); 939 } 940 } 941 942 build() { 943 Column() { 944 Button(`change desc`).onClick(() => { 945 hiTraceMeter.startTrace('change decs', 1); 946 this.desc += '!'; 947 hiTraceMeter.finishTrace('change decs', 1); 948 }) 949 950 Button(`change flag`).onClick(() => { 951 hiTraceMeter.startTrace('change flag', 1); 952 this.flag = !this.flag; 953 hiTraceMeter.finishTrace('change flag', 1); 954 }) 955 956 List({ space: 3 }) { 957 LazyForEach(this.data, (item: string, index: number) => { 958 ListItem() { 959 ChildComponent({index: index, item: item, desc: this.desc}).reuseId(index % 10 < 5 ? "1": "0") 960 } 961 }, (item: string) => item) 962 } 963 .cachedCount(5) 964 .height('60%') 965 966 if (this.flag) { 967 ChildComponent({index: -1, item: 'Hello', desc: this.desc}).reuseId( "1") 968 } 969 } 970 .height('100%') 971 } 972} 973``` 974 975## Constraints 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