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. 196. Mixed use: For example, if **LazyForEach** is used under **TabContent**, all nodes in **LazyForEach** of API version 17 or earlier are set to the active state since when switching tabs. Since API version 18, only the on-screen nodes of **LazyForEach** are set to the active state, and other nodes are set to the inactive state. 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> Mixed use of custom component freezing is supported since API version 18. 28 29## Use Scenarios 30 31### Page Routing 32 33> **NOTE** 34> 35> 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). 36 37When 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. 38For details, see the following. 39 40 41 42Page 1 43 44```ts 45import { router } from '@kit.ArkUI'; 46 47@Entry 48@Component({ freezeWhenInactive: true }) 49struct Page1 { 50 @StorageLink('PropA') @Watch("first") storageLink: number = 47; 51 52 first() { 53 console.info("first page " + `${this.storageLink}`) 54 } 55 56 build() { 57 Column() { 58 Text(`From first Page ${this.storageLink}`).fontSize(50) 59 Button('first page storageLink + 1').fontSize(30) 60 .onClick(() => { 61 this.storageLink += 1 62 }) 63 Button('go to next page').fontSize(30) 64 .onClick(() => { 65 router.pushUrl({ url: 'pages/Page2' }) 66 }) 67 } 68 } 69} 70``` 71 72Page 2 73 74```ts 75import { router } from '@kit.ArkUI'; 76 77@Entry 78@Component({ freezeWhenInactive: true }) 79struct Page2 { 80 @StorageLink('PropA') @Watch("second") storageLink2: number = 1; 81 82 second() { 83 console.info("second page: " + `${this.storageLink2}`) 84 } 85 86 build() { 87 Column() { 88 89 Text(`second Page ${this.storageLink2}`).fontSize(50) 90 Button('Change Divider.strokeWidth') 91 .onClick(() => { 92 router.back() 93 }) 94 95 Button('second page storageLink2 + 2').fontSize(30) 96 .onClick(() => { 97 this.storageLink2 += 2 98 }) 99 100 } 101 } 102} 103``` 104 105In the preceding example: 106 1071. 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. 108 1092. Through **router.pushUrl({url:'pages/second'})**, page 2 is displayed, and page 1 is hidden with its state changing from active to inactive. 110 1113. 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. 112 1134. 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. 114 115 116### TabContent 117 118- You can freeze invisible **TabContent** components in the **Tabs** container so that they do not trigger UI re-rendering. 119 120- 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. 121 122For details, see the following. 123 124 125```ts 126@Entry 127@Component 128struct TabContentTest { 129 @State @Watch("onMessageUpdated") message: number = 0; 130 private data: number[] = [0, 1] 131 132 onMessageUpdated() { 133 console.info(`TabContent message callback func ${this.message}`) 134 } 135 136 build() { 137 Row() { 138 Column() { 139 Button('change message').onClick(() => { 140 this.message++ 141 }) 142 143 Tabs() { 144 ForEach(this.data, (item: number) => { 145 TabContent() { 146 FreezeChild({ message: this.message, index: item }) 147 }.tabBar(`tab${item}`) 148 }, (item: number) => item.toString()) 149 } 150 } 151 .width('100%') 152 } 153 .height('100%') 154 } 155} 156 157@Component({ freezeWhenInactive: true }) 158struct FreezeChild { 159 @Link @Watch("onMessageUpdated") message: number 160 private index: number = 0 161 162 onMessageUpdated() { 163 console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`) 164 } 165 166 build() { 167 Text("message" + `${this.message}, index: ${this.index}`) 168 .fontSize(50) 169 .fontWeight(FontWeight.Bold) 170 } 171} 172``` 173 174In the preceding example: 175 1761. When **change message** is clicked, the value of **message** changes, and the @Watch decorated **onMessageUpdated** method of the **TabContent** component being displayed is called. 177 1782. 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. 179 1803. 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. 181 182 183 184 185### LazyForEach 186 187- You can freeze custom components cached in **LazyForEach** so that they do not trigger UI re-rendering. 188 189```ts 190// Basic implementation of IDataSource used to listening for data. 191class BasicDataSource implements IDataSource { 192 private listeners: DataChangeListener[] = []; 193 private originDataArray: string[] = []; 194 195 public totalCount(): number { 196 return 0; 197 } 198 199 public getData(index: number): string { 200 return this.originDataArray[index]; 201 } 202 203 // This method is called by the framework to add a listener to the LazyForEach data source. 204 registerDataChangeListener(listener: DataChangeListener): void { 205 if (this.listeners.indexOf(listener) < 0) { 206 console.info('add listener'); 207 this.listeners.push(listener); 208 } 209 } 210 211 // This method is called by the framework to remove the listener from the LazyForEach data source. 212 unregisterDataChangeListener(listener: DataChangeListener): void { 213 const pos = this.listeners.indexOf(listener); 214 if (pos >= 0) { 215 console.info('remove listener'); 216 this.listeners.splice(pos, 1); 217 } 218 } 219 220 // Notify LazyForEach that all child components need to be reloaded. 221 notifyDataReload(): void { 222 this.listeners.forEach(listener => { 223 listener.onDataReloaded(); 224 }) 225 } 226 227 // Notify LazyForEach that a child component needs to be added for the data item with the specified index. 228 notifyDataAdd(index: number): void { 229 this.listeners.forEach(listener => { 230 listener.onDataAdd(index); 231 }) 232 } 233 234 // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt. 235 notifyDataChange(index: number): void { 236 this.listeners.forEach(listener => { 237 listener.onDataChange(index); 238 }) 239 } 240 241 // Notify LazyForEach that the child component that matches the specified index needs to be deleted. 242 notifyDataDelete(index: number): void { 243 this.listeners.forEach(listener => { 244 listener.onDataDelete(index); 245 }) 246 } 247} 248 249class MyDataSource extends BasicDataSource { 250 private dataArray: string[] = []; 251 252 public totalCount(): number { 253 return this.dataArray.length; 254 } 255 256 public getData(index: number): string { 257 return this.dataArray[index]; 258 } 259 260 public addData(index: number, data: string): void { 261 this.dataArray.splice(index, 0, data); 262 this.notifyDataAdd(index); 263 } 264 265 public pushData(data: string): void { 266 this.dataArray.push(data); 267 this.notifyDataAdd(this.dataArray.length - 1); 268 } 269} 270 271@Entry 272@Component 273struct LforEachTest { 274 private data: MyDataSource = new MyDataSource(); 275 @State @Watch("onMessageUpdated") message: number = 0; 276 277 onMessageUpdated() { 278 console.info(`LazyforEach message callback func ${this.message}`) 279 } 280 281 aboutToAppear() { 282 for (let i = 0; i <= 20; i++) { 283 this.data.pushData(`Hello ${i}`) 284 } 285 } 286 287 build() { 288 Column() { 289 Button('change message').onClick(() => { 290 this.message++ 291 }) 292 List({ space: 3 }) { 293 LazyForEach(this.data, (item: string) => { 294 ListItem() { 295 FreezeChild({ message: this.message, index: item }) 296 } 297 }, (item: string) => item) 298 }.cachedCount(5).height(500) 299 } 300 301 } 302} 303 304@Component({ freezeWhenInactive: true }) 305struct FreezeChild { 306 @Link @Watch("onMessageUpdated") message: number; 307 private index: string = ""; 308 309 aboutToAppear() { 310 console.info(`FreezeChild aboutToAppear index: ${this.index}`) 311 } 312 313 onMessageUpdated() { 314 console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`) 315 } 316 317 build() { 318 Text("message" + `${this.message}, index: ${this.index}`) 319 .width('90%') 320 .height(160) 321 .backgroundColor(0xAFEEEE) 322 .textAlign(TextAlign.Center) 323 .fontSize(30) 324 .fontWeight(FontWeight.Bold) 325 } 326} 327``` 328 329In the preceding example: 330 3311. 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.) 332 3332. 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. 334 3353. 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. 336 337 338 339### Navigation 340 341- 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. 342 343- 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. 344 345```ts 346@Entry 347@Component 348struct MyNavigationTestStack { 349 @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 350 @State @Watch("info") message: number = 0; 351 @State logNumber: number = 0; 352 353 info() { 354 console.info(`freeze-test MyNavigation message callback ${this.message}`); 355 } 356 357 @Builder 358 PageMap(name: string) { 359 if (name === 'pageOne') { 360 pageOneStack({ message: this.message, logNumber: this.logNumber }) 361 } else if (name === 'pageTwo') { 362 pageTwoStack({ message: this.message, logNumber: this.logNumber }) 363 } else if (name === 'pageThree') { 364 pageThreeStack({ message: this.message, logNumber: this.logNumber }) 365 } 366 } 367 368 build() { 369 Column() { 370 Button('change message') 371 .onClick(() => { 372 this.message++; 373 }) 374 Navigation(this.pageInfo) { 375 Column() { 376 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 377 .width('80%') 378 .height(40) 379 .margin(20) 380 .onClick(() => { 381 this.pageInfo.pushPath({ name: 'pageOne' }); // Push the navigation destination page specified by name to the navigation stack. 382 }) 383 } 384 }.title('NavIndex') 385 .navDestination(this.PageMap) 386 .mode(NavigationMode.Stack) 387 } 388 } 389} 390 391@Component 392struct pageOneStack { 393 @Consume('pageInfo') pageInfo: NavPathStack; 394 @State index: number = 1; 395 @Link message: number; 396 @Link logNumber: number; 397 398 build() { 399 NavDestination() { 400 Column() { 401 NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber }) 402 Text("cur stack size:" + `${this.pageInfo.size()}`) 403 .fontSize(30) 404 .fontWeight(FontWeight.Bold) 405 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 406 .width('80%') 407 .height(40) 408 .margin(20) 409 .onClick(() => { 410 this.pageInfo.pushPathByName('pageTwo', null); 411 }) 412 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 413 .width('80%') 414 .height(40) 415 .margin(20) 416 .onClick(() => { 417 this.pageInfo.pop(); 418 }) 419 }.width('100%').height('100%') 420 }.title('pageOne') 421 .onBackPressed(() => { 422 this.pageInfo.pop(); 423 return true; 424 }) 425 } 426} 427 428@Component 429struct pageTwoStack { 430 @Consume('pageInfo') pageInfo: NavPathStack; 431 @State index: number = 2; 432 @Link message: number; 433 @Link logNumber: number; 434 435 build() { 436 NavDestination() { 437 Column() { 438 NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber }) 439 Text("cur stack size:" + `${this.pageInfo.size()}`) 440 .fontSize(30) 441 .fontWeight(FontWeight.Bold) 442 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 443 .width('80%') 444 .height(40) 445 .margin(20) 446 .onClick(() => { 447 this.pageInfo.pushPathByName('pageThree', null); 448 }) 449 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 450 .width('80%') 451 .height(40) 452 .margin(20) 453 .onClick(() => { 454 this.pageInfo.pop(); 455 }) 456 }.width('100%').height('100%') 457 }.title('pageTwo') 458 .onBackPressed(() => { 459 this.pageInfo.pop(); 460 return true; 461 }) 462 } 463} 464 465@Component 466struct pageThreeStack { 467 @Consume('pageInfo') pageInfo: NavPathStack; 468 @State index: number = 3; 469 @Link message: number; 470 @Link logNumber: number; 471 472 build() { 473 NavDestination() { 474 Column() { 475 NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber }) 476 Text("cur stack size:" + `${this.pageInfo.size()}`) 477 .fontSize(30) 478 .fontWeight(FontWeight.Bold) 479 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 480 .width('80%') 481 .height(40) 482 .margin(20) 483 .onClick(() => { 484 this.pageInfo.pushPathByName('pageOne', null); 485 }) 486 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 487 .width('80%') 488 .height(40) 489 .margin(20) 490 .onClick(() => { 491 this.pageInfo.pop(); 492 }) 493 }.width('100%').height('100%') 494 }.title('pageThree') 495 .onBackPressed(() => { 496 this.pageInfo.pop(); 497 return true; 498 }) 499 } 500} 501 502@Component({ freezeWhenInactive: true }) 503struct NavigationContentMsgStack { 504 @Link @Watch("info") message: number; 505 @Link index: number; 506 @Link logNumber: number; 507 508 info() { 509 console.info(`freeze-test NavigationContent message callback ${this.message}`); 510 console.info(`freeze-test ---- called by content ${this.index}`); 511 this.logNumber++; 512 } 513 514 build() { 515 Column() { 516 Text("msg:" + `${this.message}`) 517 .fontSize(30) 518 .fontWeight(FontWeight.Bold) 519 Text("log number:" + `${this.logNumber}`) 520 .fontSize(30) 521 .fontWeight(FontWeight.Bold) 522 } 523 } 524} 525``` 526 527In the preceding example: 528 5291. When **change message** is clicked, the value of **message** changes, and the @Watch decorated **info** method of the **MyNavigationTestStack** component being displayed is called. 530 5312. When **Next Page** is clicked, **PageOne** is displayed, and the **PageOneStack** node is created. 532 5333. 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. 534 5354. When **Next Page** is clicked again, **PageTwo** is displayed, and the **pageTwoStack** node is created. 536 5375. 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. 538 5396. When **Next Page** is clicked again, **PageThree** is displayed, and the **pageThreeStack** node is created. 540 5417. 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. 542 5438. When **Back Page** is clicked, **PageTwo** is displayed, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageTwoStack** is called. 544 5459. 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. 546 54710. When **Back Page** is clicked again, the initial page is displayed, and no method is called. 548 549 550 551### Reusing Components 552 553[Components reuse](./arkts-reusable.md) 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. 554 555#### Mixed Use of Component Reuse, if, and Component Freezing 556The 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. 557The procedure is as follows: 5581. Click **change flag** and change the value of **flag** to **false**. 559 - 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. 560 - **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. 5612. Click **change desc** to trigger the change of the member variable **desc** of **Page**. 562 - The change of \@State decorated **desc** will be notified to \@Link decorated **desc** of **ChildComponent**. 563 - 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. 5643. Click **change flag** again and change the value of **flag** to **true**. 565 - **ChildComponent** is attached to the component tree from the reuse pool. 566 - 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**. 567 - 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})**. 568 569 570```ts 571@Reusable 572@Component({freezeWhenInactive: true}) 573struct ChildComponent { 574 @Link @Watch('descChange') desc: string; 575 @State count: number = 0; 576 descChange() { 577 console.info(`ChildComponent messageChange ${this.desc}`); 578 } 579 580 aboutToReuse(params: Record<string, ESObject>): void { 581 this.count = params.count as number; 582 } 583 584 aboutToRecycle(): void { 585 console.info(`ChildComponent has been recycled`); 586 } 587 build() { 588 Column() { 589 Text(`ChildComponent desc: ${this.desc}`) 590 .fontSize(20) 591 Text(`ChildComponent count ${this.count}`) 592 .fontSize(20) 593 }.border({width: 2, color: Color.Pink}) 594 } 595} 596 597@Entry 598@Component 599struct Page { 600 @State desc: string = 'Hello World'; 601 @State flag: boolean = true; 602 @State count: number = 0; 603 build() { 604 Column() { 605 Button(`change desc`).onClick(() => { 606 this.desc += '!'; 607 }) 608 Button(`change flag`).onClick(() => { 609 this.count++; 610 this.flag =! this.flag; 611 }) 612 if (this.flag) { 613 ChildComponent({desc: this.desc, count: this.count}) 614 } 615 } 616 .height('100%') 617 } 618} 619``` 620#### Mixed Use of LazyForEach, Component Reuse, and Component Freezing 621In 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. 622However, 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**. 623In 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. 624 625Example: 6261. Swipe the list to the position whose index is 14. There are 15 **ChildComponent** in the visible area on the current page. 6272. During swiping: 628 - **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** cache 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. 629 - 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. 6303. Click **change desc** to trigger the change of the member variable **desc** of **Page**. 631 - The change of \@State decorated **desc** will be notified to \@Link decorated **desc** of **ChildComponent**. 632 - **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. 633 634 635For details, see the following. 636 637You can listen for the changes by \@Trace, only 15 **ChildComponent** nodes are re-rendered. 638 639A complete sample code is as follows: 640```ts 641import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'; 642// Basic implementation of IDataSource used to listening for data. 643class BasicDataSource implements IDataSource { 644 private listeners: DataChangeListener[] = []; 645 private originDataArray: string[] = []; 646 647 public totalCount(): number { 648 return 0; 649 } 650 651 public getData(index: number): string { 652 return this.originDataArray[index]; 653 } 654 655 // This method is called by the framework to add a listener to the LazyForEach data source. 656 registerDataChangeListener(listener: DataChangeListener): void { 657 if (this.listeners.indexOf(listener) < 0) { 658 console.info('add listener'); 659 this.listeners.push(listener); 660 } 661 } 662 663 // This method is called by the framework to remove the listener from the LazyForEach data source. 664 unregisterDataChangeListener(listener: DataChangeListener): void { 665 const pos = this.listeners.indexOf(listener); 666 if (pos >= 0) { 667 console.info('remove listener'); 668 this.listeners.splice(pos, 1); 669 } 670 } 671 672 // Notify LazyForEach that all child components need to be reloaded. 673 notifyDataReload(): void { 674 this.listeners.forEach(listener => { 675 listener.onDataReloaded(); 676 }) 677 } 678 679 // Notify LazyForEach that a child component needs to be added for the data item with the specified index. 680 notifyDataAdd(index: number): void { 681 this.listeners.forEach(listener => { 682 listener.onDataAdd(index); 683 }) 684 } 685 686 // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt. 687 notifyDataChange(index: number): void { 688 this.listeners.forEach(listener => { 689 listener.onDataChange(index); 690 }) 691 } 692 693 // Notify LazyForEach that the child component that matches the specified index needs to be deleted. 694 notifyDataDelete(index: number): void { 695 this.listeners.forEach(listener => { 696 listener.onDataDelete(index); 697 }) 698 } 699 700 // Notify LazyForEach that data needs to be swapped between the from and to positions. 701 notifyDataMove(from: number, to: number): void { 702 this.listeners.forEach(listener => { 703 listener.onDataMove(from, to); 704 }) 705 } 706} 707 708class MyDataSource extends BasicDataSource { 709 private dataArray: string[] = []; 710 711 public totalCount(): number { 712 return this.dataArray.length; 713 } 714 715 public getData(index: number): string { 716 return this.dataArray[index]; 717 } 718 719 public addData(index: number, data: string): void { 720 this.dataArray.splice(index, 0, data); 721 this.notifyDataAdd(index); 722 } 723 724 public pushData(data: string): void { 725 this.dataArray.push(data); 726 this.notifyDataAdd(this.dataArray.length - 1); 727 } 728} 729 730@Reusable 731@Component({freezeWhenInactive: true}) 732struct ChildComponent { 733 @Link @Watch('descChange') desc: string; 734 @State item: string = ''; 735 @State index: number = 0; 736 descChange() { 737 console.info(`ChildComponent messageChange ${this.desc}`); 738 } 739 740 aboutToReuse(params: Record<string, ESObject>): void { 741 this.item = params.item; 742 this.index = params.index; 743 } 744 745 aboutToRecycle(): void { 746 console.info(`ChildComponent has been recycled`); 747 } 748 build() { 749 Column() { 750 Text(`ChildComponent index: ${this.index} item: ${this.item}`) 751 .fontSize(20) 752 Text(`desc: ${this.desc}`) 753 .fontSize(20) 754 }.border({width: 2, color: Color.Pink}) 755 } 756} 757 758@Entry 759@Component 760struct Page { 761 @State desc: string = 'Hello World'; 762 private data: MyDataSource = new MyDataSource(); 763 764 aboutToAppear() { 765 for (let i = 0; i < 50; i++) { 766 this.data.pushData(`Hello ${i}`); 767 } 768 } 769 770 build() { 771 Column() { 772 Button(`change desc`).onClick(() => { 773 hiTraceMeter.startTrace('change decs', 1); 774 this.desc += '!'; 775 hiTraceMeter.finishTrace('change decs', 1); 776 }) 777 List({ space: 3 }) { 778 LazyForEach(this.data, (item: string, index: number) => { 779 ListItem() { 780 ChildComponent({index: index, item: item, desc: this.desc}).reuseId(index % 10 < 5 ? "1": "0") 781 } 782 }, (item: string) => item) 783 }.cachedCount(5) 784 } 785 .height('100%') 786 } 787} 788``` 789#### Mixed Use of LazyForEach, if, Component Reuse, and Component Freezing 790 791 Under the same parent custom component, reusable nodes may enter the reuse pool in different ways. For example: 792- Detaching from the cache area of LazyForEach by swiping. 793- Notifying the subnodes to detach by switching the if condition. 794 795In the following example: 7961. 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**. 7972. 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. 7983. In this case, the nodes detached through **LazyForEach** or **if** all enter the reuse pool under the **Page** node. 7994. Click **change desc** to update only the nine **ChildComponent** nodes on the page. For details, see figures below. 8005. 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. 8016. Click **change desc** again. The nodes attached through **if** and **LazyForEach** from the reuse pool can be re-rendered. 802 803Trace for component freezing enabled 804 805 806 807Trace for component freezing disabled 808 809 810 811 812A complete example is as follows: 813``` 814import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'; 815class BasicDataSource implements IDataSource { 816 private listeners: DataChangeListener[] = []; 817 private originDataArray: string[] = []; 818 819 public totalCount(): number { 820 return 0; 821 } 822 823 public getData(index: number): string { 824 return this.originDataArray[index]; 825 } 826 827 // This method is called by the framework to add a listener to the LazyForEach data source. 828 registerDataChangeListener(listener: DataChangeListener): void { 829 if (this.listeners.indexOf(listener) < 0) { 830 console.info('add listener'); 831 this.listeners.push(listener); 832 } 833 } 834 835 // This method is called by the framework to remove the listener from the LazyForEach data source. 836 unregisterDataChangeListener(listener: DataChangeListener): void { 837 const pos = this.listeners.indexOf(listener); 838 if (pos >= 0) { 839 console.info('remove listener'); 840 this.listeners.splice(pos, 1); 841 } 842 } 843 844 // Notify LazyForEach that all child components need to be reloaded. 845 notifyDataReload(): void { 846 this.listeners.forEach(listener => { 847 listener.onDataReloaded(); 848 }) 849 } 850 851 // Notify LazyForEach that a child component needs to be added for the data item with the specified index. 852 notifyDataAdd(index: number): void { 853 this.listeners.forEach(listener => { 854 listener.onDataAdd(index); 855 }) 856 } 857 858 // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt. 859 notifyDataChange(index: number): void { 860 this.listeners.forEach(listener => { 861 listener.onDataChange(index); 862 }) 863 } 864 865 // Notify LazyForEach that the child component that matches the specified index needs to be deleted. 866 notifyDataDelete(index: number): void { 867 this.listeners.forEach(listener => { 868 listener.onDataDelete(index); 869 }) 870 } 871 872 // Notify LazyForEach that data needs to be swapped between the from and to positions. 873 notifyDataMove(from: number, to: number): void { 874 this.listeners.forEach(listener => { 875 listener.onDataMove(from, to); 876 }) 877 } 878} 879 880class MyDataSource extends BasicDataSource { 881 private dataArray: string[] = []; 882 883 public totalCount(): number { 884 return this.dataArray.length; 885 } 886 887 public getData(index: number): string { 888 return this.dataArray[index]; 889 } 890 891 public addData(index: number, data: string): void { 892 this.dataArray.splice(index, 0, data); 893 this.notifyDataAdd(index); 894 } 895 896 public pushData(data: string): void { 897 this.dataArray.push(data); 898 this.notifyDataAdd(this.dataArray.length - 1); 899 } 900} 901 902@Reusable 903@Component({freezeWhenInactive: true}) 904struct ChildComponent { 905 @Link @Watch('descChange') desc: string; 906 @State item: string = ''; 907 @State index: number = 0; 908 descChange() { 909 console.info(`ChildComponent messageChange ${this.desc}`); 910 } 911 912 aboutToReuse(params: Record<string, ESObject>): void { 913 this.item = params.item; 914 this.index = params.index; 915 } 916 917 aboutToRecycle(): void { 918 console.info(`ChildComponent has been recycled`); 919 } 920 build() { 921 Column() { 922 Text(`ChildComponent index: ${this.index} item: ${this.item}`) 923 .fontSize(20) 924 Text(`desc: ${this.desc}`) 925 .fontSize(20) 926 }.border({width: 2, color: Color.Pink}) 927 } 928} 929 930@Entry 931@Component 932struct Page { 933 @State desc: string = 'Hello World'; 934 @State flag: boolean = true; 935 private data: MyDataSource = new MyDataSource(); 936 937 aboutToAppear() { 938 for (let i = 0; i < 50; i++) { 939 this.data.pushData(`Hello ${i}`); 940 } 941 } 942 943 build() { 944 Column() { 945 Button(`change desc`).onClick(() => { 946 hiTraceMeter.startTrace('change decs', 1); 947 this.desc += '!'; 948 hiTraceMeter.finishTrace('change decs', 1); 949 }) 950 951 Button(`change flag`).onClick(() => { 952 hiTraceMeter.startTrace('change flag', 1); 953 this.flag = !this.flag; 954 hiTraceMeter.finishTrace('change flag', 1); 955 }) 956 957 List({ space: 3 }) { 958 LazyForEach(this.data, (item: string, index: number) => { 959 ListItem() { 960 ChildComponent({index: index, item: item, desc: this.desc}).reuseId(index % 10 < 5 ? "1": "0") 961 } 962 }, (item: string) => item) 963 } 964 .cachedCount(5) 965 .height('60%') 966 967 if (this.flag) { 968 ChildComponent({index: -1, item: 'Hello', desc: this.desc}).reuseId( "1") 969 } 970 } 971 .height('100%') 972 } 973} 974``` 975 976### Mixing the Use of Components 977 978In the scenario where mixed use of component freezing is supported, the freezing behavior varies according to the API version. Set the component freezing flag for the parent component. In API version 17 or earlier, when the parent component is unfrozen, all nodes of its child components are unfrozen. Since API version 18, when the parent component is unfrozen, only the on-screen nodes of the child component are unfrozen. 979 980#### Mixed Use of Navigation and TabContent 981 982The sample code is as follows: 983 984```ts 985// index.ets 986@Component 987struct ChildOfParamComponent { 988 @Prop @Watch('onChange') child_val: number; 989 990 onChange() { 991 console.log(`Appmonitor ChildOfParamComponent: child_val changed:${this.child_val}`); 992 } 993 994 build() { 995 Column() { 996 Text(`Child Param: ${this.child_val}`); 997 } 998 } 999} 1000 1001@Component 1002struct ParamComponent { 1003 @Prop @Watch('onChange') paramVal: number; 1004 1005 onChange() { 1006 console.log(`Appmonitor ParamComponent: paramVal changed:${this.paramVal}`); 1007 } 1008 1009 build() { 1010 Column() { 1011 Text(`val: ${this.paramVal}`) 1012 ChildOfParamComponent({child_val: this.paramVal}); 1013 } 1014 } 1015} 1016 1017 1018 1019@Component 1020struct DelayComponent { 1021 @Prop @Watch('onChange') delayVal: number; 1022 1023 onChange() { 1024 console.log(`Appmonitor ParamComponent: delayVal changed:${this.delayVal}`); 1025 } 1026 1027 1028 build() { 1029 Column() { 1030 Text(`Delay Param: ${this.delayVal}`); 1031 } 1032 } 1033} 1034 1035@Component ({freezeWhenInactive: true}) 1036struct TabsComponent { 1037 private controller: TabsController = new TabsController(); 1038 @State @Watch('onChange') tabState: number = 47; 1039 1040 onChange() { 1041 console.log(`Appmonitor TabsComponent: tabState changed:${this.tabState}`); 1042 } 1043 1044 build() { 1045 Column({space: 10}) { 1046 Button(`Incr state ${this.tabState}`) 1047 .fontSize(25) 1048 .onClick(() => { 1049 console.log('Button increment state value'); 1050 this.tabState = this.tabState + 1; 1051 }) 1052 1053 Tabs({ barPosition: BarPosition.Start, index: 0, controller: this.controller}) { 1054 TabContent() { 1055 ParamComponent({paramVal: this.tabState}); 1056 }.tabBar('Update') 1057 TabContent() { 1058 DelayComponent({delayVal: this.tabState}); 1059 }.tabBar('DelayUpdate') 1060 } 1061 .vertical(false) 1062 .scrollable(true) 1063 .barMode(BarMode.Fixed) 1064 .barWidth(400).barHeight(150).animationDuration(400) 1065 .width('100%') 1066 .height(200) 1067 .backgroundColor(0xF5F5F5) 1068 } 1069 } 1070} 1071 1072@Entry 1073@Component 1074struct MyNavigationTestStack { 1075 @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 1076 1077 @Builder 1078 PageMap(name: string) { 1079 if (name === 'pageOne') { 1080 pageOneStack() 1081 } else if (name === 'pageTwo') { 1082 pageTwoStack() 1083 } 1084 } 1085 1086 build() { 1087 Column() { 1088 Navigation(this.pageInfo) { 1089 Column() { 1090 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 1091 .width('80%') 1092 .height(40) 1093 .margin(20) 1094 .onClick(() => { 1095 this.pageInfo.pushPath({ name: 'pageOne' }); // Push the navigation destination page specified by name to the navigation stack. 1096 }) 1097 } 1098 }.title('NavIndex') 1099 .navDestination(this.PageMap) 1100 .mode(NavigationMode.Stack) 1101 } 1102 } 1103} 1104 1105@Component 1106struct pageOneStack { 1107 @Consume('pageInfo') pageInfo: NavPathStack; 1108 1109 build() { 1110 NavDestination() { 1111 Column() { 1112 TabsComponent(); 1113 1114 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 1115 .width('80%') 1116 .height(40) 1117 .margin(20) 1118 .onClick(() => { 1119 this.pageInfo.pushPathByName('pageTwo', null); 1120 }) 1121 }.width('100%').height('100%') 1122 }.title('pageOne') 1123 .onBackPressed(() => { 1124 this.pageInfo.pop(); 1125 return true; 1126 }) 1127 } 1128} 1129 1130@Component 1131struct pageTwoStack { 1132 @Consume('pageInfo') pageInfo: NavPathStack; 1133 1134 build() { 1135 NavDestination() { 1136 Column() { 1137 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 1138 .width('80%') 1139 .height(40) 1140 .margin(20) 1141 .onClick(() => { 1142 this.pageInfo.pop(); 1143 }) 1144 }.width('100%').height('100%') 1145 }.title('pageTwo') 1146 .onBackPressed(() => { 1147 this.pageInfo.pop(); 1148 return true; 1149 }) 1150 } 1151} 1152``` 1153 1154Final effect 1155 1156 1157 1158Click the **Next Page** button to enter the **pageOne** page. There are two tabs on the page and the **Update** tab is displayed by default. Enable component freezing. If the **Tabcontent** tab is not selected, the state variable is not refreshed. 1159 1160Click the **Incr state** button to query **Appmonitor** in the log. Three records are displayed. 1161 1162 1163 1164Switch to the **DelayUpdate** tab and click the **Incr state** button to query **Appmonitor** in the log. Two records are displayed. The state variable in the **DelayUpdate** tab does not refresh the state variable related to the **Update** tab. 1165 1166 1167 1168For API version 17 or earlier: 1169 1170Click **Next page** to enter the next page and then return. The tab is **DelayUpdate** by default. Click **Incr state** to query **Appmonitor** in the log and four records are displayed. When the page route is returned, all tabs of **Tabcontent** are unfrozen. 1171 1172 1173 1174For API version 18 or later: 1175 1176Click **Next page** to enter the next page and then return. The tab is **DelayUpdate** by default. Click **Incr state** to query **Appmonitor** in the log and two records are displayed. When the page route is returned, only the nodes with the corresponding tabs are unfrozen. 1177 1178 1179 1180#### Page and LazyForEach 1181 1182When **Navigation** and **TabContent** are used together, the child nodes of **TabContent** are unlocked because the child component is recursively unfrozen from the parent component when the previous page is displayed. In addition, the page lifecycle **OnPageShow** shows a similar behavior. **OnPageShow** sets the root node of the current page to the active state. As a subnode of the page, **TabContent** is also set to the active state. When the screen is turned off or on, the page lifecycles **OnPageHide** and **OnPageShow** are triggered respectively. Therefore, when **LazyForEach** is used on the page, manual screen-off and screen-on can also implement the page routing effect. The sample code is as follows: 1183 1184```ts 1185import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'; 1186// Basic implementation of IDataSource used to listening for data. 1187class BasicDataSource implements IDataSource { 1188 private listeners: DataChangeListener[] = []; 1189 private originDataArray: string[] = []; 1190 1191 public totalCount(): number { 1192 return 0; 1193 } 1194 1195 public getData(index: number): string { 1196 return this.originDataArray[index]; 1197 } 1198 1199 // This method is called by the framework to add a listener to the LazyForEach data source. 1200 registerDataChangeListener(listener: DataChangeListener): void { 1201 if (this.listeners.indexOf(listener) < 0) { 1202 console.info('add listener'); 1203 this.listeners.push(listener); 1204 } 1205 } 1206 1207 // This method is called by the framework to remove the listener from the LazyForEach data source. 1208 unregisterDataChangeListener(listener: DataChangeListener): void { 1209 const pos = this.listeners.indexOf(listener); 1210 if (pos >= 0) { 1211 console.info('remove listener'); 1212 this.listeners.splice(pos, 1); 1213 } 1214 } 1215 1216 // Notify LazyForEach that all child components need to be reloaded. 1217 notifyDataReload(): void { 1218 this.listeners.forEach(listener => { 1219 listener.onDataReloaded(); 1220 }) 1221 } 1222 1223 // Notify LazyForEach that a child component needs to be added for the data item with the specified index. 1224 notifyDataAdd(index: number): void { 1225 this.listeners.forEach(listener => { 1226 listener.onDataAdd(index); 1227 }) 1228 } 1229 1230 // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt. 1231 notifyDataChange(index: number): void { 1232 this.listeners.forEach(listener => { 1233 listener.onDataChange(index); 1234 }) 1235 } 1236 1237 // Notify LazyForEach that the child component that matches the specified index needs to be deleted. 1238 notifyDataDelete(index: number): void { 1239 this.listeners.forEach(listener => { 1240 listener.onDataDelete(index); 1241 }) 1242 } 1243 1244 // Notify LazyForEach that data needs to be swapped between the from and to positions. 1245 notifyDataMove(from: number, to: number): void { 1246 this.listeners.forEach(listener => { 1247 listener.onDataMove(from, to); 1248 }) 1249 } 1250} 1251 1252class MyDataSource extends BasicDataSource { 1253 private dataArray: string[] = []; 1254 1255 public totalCount(): number { 1256 return this.dataArray.length; 1257 } 1258 1259 public getData(index: number): string { 1260 return this.dataArray[index]; 1261 } 1262 1263 public addData(index: number, data: string): void { 1264 this.dataArray.splice(index, 0, data); 1265 this.notifyDataAdd(index); 1266 } 1267 1268 public pushData(data: string): void { 1269 this.dataArray.push(data); 1270 this.notifyDataAdd(this.dataArray.length - 1); 1271 } 1272} 1273 1274@Reusable 1275@Component({freezeWhenInactive: true}) 1276struct ChildComponent { 1277 @State desc: string = ''; 1278 @Link @Watch('sumChange') sum: number; 1279 1280 sumChange() { 1281 console.info(`sum: Change ${this.sum}`); 1282 } 1283 1284 aboutToReuse(params: Record<string, Object>): void { 1285 this.desc = params.desc as string; 1286 this.sum = params.sum as number; 1287 } 1288 1289 aboutToRecycle(): void { 1290 console.info(`ChildComponent has been recycled`); 1291 } 1292 build() { 1293 Column() { 1294 Divider() 1295 .color('#ff11acb8') 1296 Text('Child component:' + this.desc) 1297 .fontSize(30) 1298 .fontWeight(30) 1299 Text(`${this.sum}`) 1300 .fontSize(30) 1301 .fontWeight(30) 1302 } 1303 } 1304} 1305 1306@Entry 1307@Component ({freezeWhenInactive: true}) 1308struct Page { 1309 private data: MyDataSource = new MyDataSource(); 1310 @State sum: number = 0; 1311 @State desc: string = ''; 1312 1313 aboutToAppear() { 1314 for (let index = 0; index < 20; index++) { 1315 this.data.pushData(index.toString()); 1316 } 1317 } 1318 1319 build() { 1320 Column() { 1321 Button(`add sum`).onClick(() => { 1322 this.sum++; 1323 }) 1324 .fontSize(30) 1325 .margin(20) 1326 List() { 1327 LazyForEach(this.data, (item: string) => { 1328 ListItem() { 1329 ChildComponent({desc: item, sum: this.sum}); 1330 } 1331 .width('100%') 1332 .height(100) 1333 }, (item: string) => item) 1334 }.cachedCount(5) 1335 } 1336 .height('100%') 1337 .width('100%') 1338 } 1339} 1340``` 1341 1342As described in the mixed use scenario, the nodes of **LazyForEach** include the on-screen node and **cachedCount** node. 1343 1344 1345 1346Swipe down **LazyForEach** to add nodes to **cachedCount**. Click the **add sum** button to search for the log "sum: Change." and eight records are displayed. 1347 1348 1349 1350For API version 17 or earlier: 1351 1352Turn off and on the screen to trigger **OnPageShow** and then click **add sum**. The number of printed records is equal to the number of on-screen nodes and the **cachedCount** nodes. 1353 1354 1355 1356For API version 18 or later: 1357 1358Turn off and on the screen to trigger **OnPageShow** and then click **add sum**. Only the number of on-screen nodes is displayed, and the **cachedCount** nodes are not unfrozen. 1359 1360 1361 1362## Constraints 1363 1364As 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. 1365 1366``` 1367import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI'; 1368 1369// Define a Params class to pass parameters. 1370class Params { 1371 index: number = 0; 1372 1373 constructor(index: number) { 1374 this.index = index; 1375 } 1376} 1377 1378// Define a buildNodeChild component that contains a message attribute and an index attribute. 1379@Component 1380struct buildNodeChild { 1381 @StorageProp("buildNodeTest") @Watch("onMessageUpdated") message: string = "hello world"; 1382 @State index: number = 0; 1383 1384 // Call this method when message is updated. 1385 onMessageUpdated() { 1386 console.log(`FreezeBuildNode builderNodeChild message callback func ${this.message},index: ${this.index}`); 1387 } 1388 1389 build() { 1390 Text(`buildNode Child message: ${this.message}`).fontSize(30) 1391 } 1392} 1393 1394// Define a buildText function that receives a Params parameter and constructs a Column component. 1395@Builder 1396function buildText(params: Params) { 1397 Column() { 1398 buildNodeChild({ index: params.index }) 1399 } 1400} 1401 1402// Define a TextNodeController class that is inherited from NodeController. 1403class TextNodeController extends NodeController { 1404 private textNode: BuilderNode<[Params]> | null = null; 1405 private index: number = 0; 1406 1407 // The constructor receives an index parameter. 1408 constructor(index: number) { 1409 super(); 1410 this.index = index; 1411 } 1412 1413 // Create and return a FrameNode. 1414 makeNode(context: UIContext): FrameNode | null { 1415 this.textNode = new BuilderNode(context); 1416 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.index)); 1417 return this.textNode.getFrameNode(); 1418 } 1419} 1420 1421// Define an index component that contains a message attribute and a data array. 1422@Entry 1423@Component 1424struct Index { 1425 @StorageLink("buildNodeTest") message: string = "hello"; 1426 private data: number[] = [0, 1]; 1427 1428 build() { 1429 Row() { 1430 Column() { 1431 Button("change").fontSize(30) 1432 .onClick(() => { 1433 this.message += 'a'; 1434 }) 1435 1436 Tabs() { 1437 ForEach(this.data, (item: number) => { 1438 TabContent() { 1439 FreezeBuildNode({ index: item }) 1440 }.tabBar(`tab${item}`) 1441 }, (item: number) => item.toString()) 1442 } 1443 } 1444 } 1445 .width('100%') 1446 .height('100%') 1447 } 1448} 1449 1450// Define a FreezeBuildNode component that contains a message attribute and an index attribute. 1451@Component({ freezeWhenInactive: true }) 1452struct FreezeBuildNode { 1453 @StorageProp("buildNodeTest") @Watch("onMessageUpdated") message: string = "1111"; 1454 @State index: number = 0; 1455 1456 // Call this method when message is updated. 1457 onMessageUpdated() { 1458 console.log(`FreezeBuildNode message callback func ${this.message}, index: ${this.index}`); 1459 } 1460 1461 build() { 1462 NodeContainer(new TextNodeController(this.index)) 1463 .width('100%') 1464 .height('100%') 1465 .backgroundColor('#FFF0F0F0') 1466 } 1467} 1468``` 1469 1470In the preceding example: 1471 1472Click **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. 1473 1474 1475