1# 自定义组件冻结功能 2<!--Kit: ArkUI--> 3<!--Subsystem: ArkUI--> 4<!--Owner: @liwenzhen3--> 5<!--Designer: @s10021109--> 6<!--Tester: @TerryTsao--> 7<!--Adviser: @zhang_yixin13--> 8 9当@ComponentV2装饰的自定义组件处于非激活状态时,状态变量将不响应更新,即[@Monitor](./arkts-new-monitor.md)不会调用,状态变量关联的节点不会刷新。该冻结机制在复杂UI场景下能显著优化性能,避免非激活组件因状态变量更新进行无效刷新,从而减少资源消耗。通过freezeWhenInactive属性来决定是否使用冻结功能,不传参数时默认不使用。支持的场景有:[页面路由](../../reference/apis-arkui/js-apis-router.md)、[TabContent](../../reference/apis-arkui/arkui-ts/ts-container-tabcontent.md)、[Navigation](../../reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md)、[Repeat](../../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md)。 10 11在阅读本文档前,开发者需要了解\@ComponentV2基本语法。建议提前阅读:[\@ComponentV2](./arkts-new-componentV2.md)。 12 13> **说明:** 14> 15> 从API version 12开始,支持@ComponentV2装饰的自定义组件冻结功能。 16> 17> 从API version 18开始,支持自定义组件冻结功能的混用场景冻结。 18> 19> 与@Component的组件冻结不同,@ComponentV2装饰的自定义组件不支持在LazyForEach场景下缓存节点组件冻结。 20 21## 当前支持的场景 22 23### 页面路由 24 25> **说明:** 26> 27> 本示例使用了router进行页面跳转,建议开发者使用组件导航(Navigation)代替页面路由(router)来实现页面切换。Navigation提供了更多的功能和更灵活的自定义能力。请参考[使用Navigation的组件冻结用例](#navigation)。 28 29- 当页面1调用router.pushUrl接口跳转到页面2时,页面1为隐藏不可见状态,此时如果更新页面1中的状态变量,不会触发页面1刷新。 30图示如下: 31 32 33 34页面1: 35 36```ts 37@ObservedV2 38export class Book { 39 @Trace name: string = "100"; 40 41 constructor(page: string) { 42 this.name = page; 43 } 44} 45 46@Entry 47@ComponentV2({ freezeWhenInactive: true }) 48export struct Page1 { 49 @Local bookTest: Book = new Book("A Midsummer Night’s Dream"); 50 51 @Monitor("bookTest.name") 52 onMessageChange(monitor: IMonitor) { 53 console.info(`The book name change from ${monitor.value()?.before} to ${monitor.value()?.now}`); 54 } 55 56 build() { 57 Column() { 58 Text(`Book name is ${this.bookTest.name}`).fontSize(25) 59 Button('changeBookName').fontSize(25) 60 .onClick(() => { 61 this.bookTest.name = "The Old Man and the Sea"; 62 }) 63 Button('go to next page').fontSize(25) 64 .onClick(() => { 65 this.getUIContext().getRouter().pushUrl({ url: 'pages/Page2' }); 66 setTimeout(() => { 67 this.bookTest = new Book("Jane Austen's Pride and Prejudice"); 68 }, 1000) 69 }) 70 } 71 } 72} 73``` 74 75页面2: 76 77```ts 78@Entry 79@ComponentV2 80struct Page2 { 81 build() { 82 Column() { 83 Text(`This is the page2`).fontSize(25) 84 Button('Back') 85 .onClick(() => { 86 this.getUIContext().getRouter().back(); 87 }) 88 } 89 } 90} 91``` 92 93在上面的示例中: 94 951. 点击页面1中的Button “changeBookName”,bookTest变量的name属性改变,@Monitor中注册的方法onMessageChange会被调用。 96 972. 点击页面1中的Button “go to next page”,跳转到页面2,然后延迟1s更新状态变量“bookTest”。在更新“bookTest”的时候,已经跳转到页面2,页面1处于inactive状态,[@Local](./arkts-new-local.md)装饰的状态变量bookTest将不响应更新,其@Monitor不会调用,关联的节点不会刷新。 98 99Trace如下: 100 101 102 1033. 点击“back”,页面2被销毁,页面1的状态由inactive变为active。状态变量“bookTest”的更新被观察到,@Monitor中注册的方法onMessageChange被调用,对应的Text显示内容改变。 104 105 106 107### TabContent 108 109- 对Tabs中当前不可见的TabContent进行冻结,不会触发组件的更新。 110 111- 需要注意的是:在首次渲染的时候,Tab只会创建当前正在显示的TabContent,当切换全部的TabContent后,TabContent才会被全部创建。 112 113图示如下: 114 115 116```ts 117@Entry 118@ComponentV2 119struct TabContentTest { 120 @Local message: number = 0; 121 @Local data: number[] = [0, 1]; 122 123 build() { 124 Row() { 125 Column() { 126 Button('change message').onClick(() => { 127 this.message++; 128 }) 129 130 Tabs() { 131 ForEach(this.data, (item: number) => { 132 TabContent() { 133 FreezeChild({ message: this.message, index: item }) 134 }.tabBar(`tab${item}`) 135 }, (item: number) => item.toString()) 136 } 137 } 138 .width('100%') 139 } 140 .height('100%') 141 } 142} 143 144@ComponentV2({ freezeWhenInactive: true }) 145struct FreezeChild { 146 @Param message: number = 0; 147 @Param index: number = 0; 148 149 @Monitor('message') onMessageUpdated(mon: IMonitor) { 150 console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`); 151 } 152 153 build() { 154 Text("message" + `${this.message}, index: ${this.index}`) 155 .fontSize(50) 156 .fontWeight(FontWeight.Bold) 157 } 158} 159``` 160 161在上面的示例中: 162 1631.点击“change message”更改message的值,当前正在显示的TabContent组件中的@Monitor中注册的方法onMessageUpdated被触发。 164 1652.点击TabBar“tab1”切换到另外的TabContent,TabContent状态由inactive变为active,对应的@Monitor中注册的方法onMessageUpdated被触发。 166 1673.再次点击“change message”更改message的值,仅当前显示的TabContent子组件中的@Monitor中注册的方法onMessageUpdated被触发。其他inactive的TabContent组件不会触发@Monitor。 168 169 170 171 172### Navigation 173 174- 当NavDestination不可见时,会将其子自定义组件设置成非激活态,不会触发组件的刷新。当返回该页面时,其子自定义组件重新恢复成激活态,触发@Monitor回调进行刷新。 175 176```ts 177@Entry 178@ComponentV2 179struct MyNavigationTestStack { 180 @Provider('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 181 @Local message: number = 0; 182 183 @Monitor('message') info() { 184 console.info(`freeze-test MyNavigation message callback ${this.message}`); 185 } 186 187 @Builder 188 PageMap(name: string) { 189 if (name === 'pageOne') { 190 PageOneStack({ message: this.message }) 191 } else if (name === 'pageTwo') { 192 PageTwoStack({ message: this.message }) 193 } else if (name === 'pageThree') { 194 PageThreeStack({ message: this.message }) 195 } 196 } 197 198 build() { 199 Column() { 200 Button('change message') 201 .onClick(() => { 202 this.message++; 203 }) 204 Navigation(this.pageInfo) { 205 Column() { 206 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 207 .onClick(() => { 208 this.pageInfo.pushPath({ name: 'pageOne' }); //将name指定的NavDestination页面信息入栈 209 }) 210 } 211 }.title('NavIndex') 212 .navDestination(this.PageMap) 213 .mode(NavigationMode.Stack) 214 } 215 } 216} 217 218@ComponentV2 219struct PageOneStack { 220 @Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 221 @Local index: number = 1; 222 @Param message: number = 0; 223 224 build() { 225 NavDestination() { 226 Column() { 227 NavigationContentMsgStack({ message: this.message, index: this.index }) 228 Text("cur stack size:" + `${this.pageInfo.size()}`) 229 .fontSize(30) 230 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 231 .onClick(() => { 232 this.pageInfo.pushPathByName('pageTwo', null); 233 }) 234 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 235 .onClick(() => { 236 this.pageInfo.pop(); 237 }) 238 }.width('100%').height('100%') 239 }.title('pageOne') 240 .onBackPressed(() => { 241 this.pageInfo.pop(); 242 return true; 243 }) 244 } 245} 246 247@ComponentV2 248struct PageTwoStack { 249 @Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 250 @Local index: number = 2; 251 @Param message: number = 0; 252 253 build() { 254 NavDestination() { 255 Column() { 256 NavigationContentMsgStack({ message: this.message, index: this.index }) 257 Text("cur stack size:" + `${this.pageInfo.size()}`) 258 .fontSize(30) 259 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 260 .onClick(() => { 261 this.pageInfo.pushPathByName('pageThree', null); 262 }) 263 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 264 .onClick(() => { 265 this.pageInfo.pop(); 266 }) 267 } 268 }.title('pageTwo') 269 .onBackPressed(() => { 270 this.pageInfo.pop(); 271 return true; 272 }) 273 } 274} 275 276@ComponentV2 277struct PageThreeStack { 278 @Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 279 @Local index: number = 3; 280 @Param message: number = 0; 281 282 build() { 283 NavDestination() { 284 Column() { 285 NavigationContentMsgStack({ message: this.message, index: this.index }) 286 Text("cur stack size:" + `${this.pageInfo.size()}`) 287 .fontSize(30) 288 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 289 .height(40) 290 .onClick(() => { 291 this.pageInfo.pushPathByName('pageOne', null); 292 }) 293 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 294 .height(40) 295 .onClick(() => { 296 this.pageInfo.pop(); 297 }) 298 } 299 }.title('pageThree') 300 .onBackPressed(() => { 301 this.pageInfo.pop(); 302 return true; 303 }) 304 } 305} 306 307@ComponentV2({ freezeWhenInactive: true }) 308struct NavigationContentMsgStack { 309 @Param message: number = 0; 310 @Param index: number = 0; 311 312 @Monitor('message') info() { 313 console.info(`freeze-test NavigationContent message callback ${this.message}`); 314 console.info(`freeze-test ---- called by content ${this.index}`); 315 } 316 317 build() { 318 Column() { 319 Text("msg:" + `${this.message}`) 320 .fontSize(30) 321 } 322 } 323} 324``` 325 326在上面的示例中: 327 3281.点击“change message”更改message的值,当前正在显示的MyNavigationTestStack组件中的@Monitor中注册的方法info被触发。 329 3302.点击“Next Page”切换到PageOne,创建PageOneStack节点。 331 3323.再次点击“change message”更改message的值,仅PageOneStack中的NavigationContentMsgStack子组件中的@Monitor中注册的方法info被触发。 333 3344.再次点击“Next Page”切换到PageTwo,创建PageTwoStack节点。PageOneStack节点状态由active变为inactive。 335 3365.再次点击“change message”更改message的值,仅PageTwoStack中的NavigationContentMsgStack子组件中的@Monitor中注册的方法info被触发。Navigation路由栈中非栈顶的NavDestination中的子自定义组件,将是inactive状态。@Monitor方法不会触发。 337 3386.再次点击“Next Page”切换到PageThree,创建PageThreeStack节点。PageTwoStack节点状态由active变为inactive。 339 3407.再次点击“change message”更改message的值,仅PageThreeStack中的NavigationContentMsgStack子组件中的@Monitor中注册的方法info被触发。Navigation路由栈中非栈顶的NavDestination中的子自定义组件,将是inactive状态。@Monitor方法不会触发。 341 3428.点击“Back Page”回到PageTwo,此时,PageTwoStack节点状态由inactive变为active,其NavigationContentMsgStack子组件中的@Monitor中注册的方法info被触发。 343 3449.再次点击“Back Page”回到PageOne,此时,PageOneStack节点状态由inactive变为active,其NavigationContentMsgStack子组件中的@Monitor中注册的方法info被触发。 345 34610.再次点击“Back Page”回到初始页。 347 348 349 350### Repeat 351 352> **说明:** 353> 354> Repeat从API version 18开始支持自定义组件冻结。 355 356对Repeat缓存池中的自定义组件进行冻结,避免不必要的组件刷新。建议提前阅读[Repeat节点更新/复用能力说明](./arkts-new-rendering-control-repeat.md#节点更新复用能力说明)。 357 358```ts 359@Entry 360@ComponentV2 361struct RepeatVirtualScrollFreeze { 362 @Local simpleList: Array<string> = []; 363 @Local bgColor: Color = Color.Pink; 364 365 aboutToAppear(): void { 366 for (let i = 0; i < 7; i++) { 367 this.simpleList.push(`item${i}`); 368 } 369 } 370 371 build() { 372 Column() { 373 Row() { 374 Button(`Reduce length to 5`) 375 .onClick(() => { 376 this.simpleList = this.simpleList.slice(0, 5); 377 }) 378 Button(`Change bgColor`) 379 .onClick(() => { 380 this.bgColor = this.bgColor == Color.Pink ? Color.Blue : Color.Pink; 381 }) 382 } 383 384 List() { 385 Repeat(this.simpleList) 386 .each((obj: RepeatItem<string>) => { 387 }) 388 .key((item: string, index: number) => item) 389 .virtualScroll({ totalCount: this.simpleList.length }) 390 .templateId(() => `a`) 391 .template(`a`, (ri) => { 392 ChildComponent({ 393 message: ri.item, 394 bgColor: this.bgColor 395 }) 396 }, { cachedCount: 2 }) 397 } 398 .cachedCount(0) 399 .height(500) 400 } 401 .height(`100%`) 402 } 403} 404 405// 开启组件冻结 406@ComponentV2({ freezeWhenInactive: true }) 407struct ChildComponent { 408 @Param @Require message: string = ``; 409 @Param @Require bgColor: Color = Color.Pink; 410 @Monitor(`bgColor`) 411 onBgColorChange(monitor: IMonitor) { 412 // bgColor改变时,缓存池中组件不刷新,不会打印日志 413 console.info(`repeat---bgColor change from ${monitor.value()?.before} to ${monitor.value()?.now}`); 414 } 415 416 build() { 417 Text(`[a]: ${this.message}`) 418 .fontSize(50) 419 .backgroundColor(this.bgColor) 420 } 421} 422``` 423 424在上面的示例中: 425 426点击“Reduce length to 5”后,被移除的两个组件会进入Repeat缓存池,然后点击“Change bgColor”更改bgColor的值触发节点刷新。 427 428开启组件冻结(freezeWhenInactive: true),只有剩余节点中@Monitor装饰的方法onBgColorChange被触发,如示例中屏上的5个节点会刷新并打印5条日志,缓存池中的节点则不会。 429 430 431 432```ts 433// 关闭组件冻结 434@ComponentV2({ freezeWhenInactive: false }) 435struct ChildComponent { 436 @Param @Require message: string = ``; 437 @Param @Require bgColor: Color = Color.Pink; 438 @Monitor(`bgColor`) 439 onBgColorChange(monitor: IMonitor) { 440 // bgColor改变时,缓存池组件也会刷新,并打印日志 441 console.info(`repeat---bgColor change from ${monitor.value()?.before} to ${monitor.value()?.now}`); 442 } 443 444 build() { 445 Text(`[a]: ${this.message}`) 446 .fontSize(50) 447 .backgroundColor(this.bgColor) 448 } 449} 450``` 451 452不开启组件冻结(freezeWhenInactive: false,当未指定freezeWhenInactive参数时默认不开启组件冻结),剩余节点和缓存池节点中@Monitor装饰的方法onBgColorChange都会被触发,即会有7个节点会刷新并打印7条日志。 453 454 455 456### 仅子组件开启组件冻结 457 458如果开发者只想冻结某个子组件,可以选择只在子组件设置freezeWhenInactive为true。 459 460```ts 461// Page1.ets 462@ObservedV2 463class Book { 464 @Trace name: string = 'TS'; 465 466 constructor(name: string) { 467 this.name = name; 468 } 469} 470 471@Entry 472@ComponentV2 473struct Page1 { 474 pageInfo: NavPathStack = new NavPathStack(); 475 476 build() { 477 Column() { 478 Navigation(this.pageInfo) { 479 Child() 480 481 Button('Go to next page').fontSize(30) 482 .onClick(() => { 483 this.pageInfo.pushPathByName('Page2', null); 484 }) 485 } 486 } 487 } 488} 489 490@ComponentV2({ freezeWhenInactive: true }) 491export struct Child { 492 @Local bookTest: Book = new Book(`A Midsummer Night's Dream`); 493 494 @Monitor('bookTest.name') 495 onMessageChange(monitor: IMonitor) { 496 console.info(`The book name change from ${monitor.value()?.before} to ${monitor.value()?.now}`); 497 } 498 499 textUpdate(): number { 500 console.info('The text is update'); 501 return 25; 502 } 503 504 build() { 505 Column() { 506 Text(`The book name is ${this.bookTest.name}`).fontSize(this.textUpdate()) 507 508 Button('change BookName') 509 .onClick(() => { 510 setTimeout(() => { 511 this.bookTest = new Book("Jane Austen's Pride and Prejudice"); 512 }, 3000); 513 }) 514 } 515 } 516} 517``` 518 519```ts 520// Page2.ets 521@Builder 522function Page2Builder() { 523 Page2() 524} 525 526@ComponentV2 527struct Page2 { 528 pathStack: NavPathStack = new NavPathStack(); 529 530 build() { 531 NavDestination() { 532 Column() { 533 Text('This is the Page2') 534 535 Button('Back').fontSize(30) 536 .onClick(() => { 537 this.pathStack.pop(); 538 }) 539 } 540 }.onReady((context: NavDestinationContext) => { 541 this.pathStack = context.pathStack; 542 }) 543 } 544} 545``` 546 547使用Navigation时,需要添加配置系统路由表文件src/main/resources/base/profile/route_map.json,并替换pageSourceFile为Page2页面的路径,并且在module.json5中添加:"routerMap": "$profile:route_map"。 548 549```json 550{ 551 "routerMap": [ 552 { 553 "name": "Page2", 554 "pageSourceFile": "src/main/ets/pages/Page2.ets", 555 "buildFunction": "Page2Builder", 556 "data": { 557 "description" : "This is the Page2" 558 } 559 } 560 ] 561} 562``` 563 564上述示例: 565- Page1的子组件Child,设置`freezeWhenInactive: true`, 开启了组件冻结功能。 566- 点击`Button('change BookName')`,然后3s内点击`Button('Go to next page')`。在更新bookTest的时候,已经跳转到Page2,Page1的组件处于inactive状态,又因为Child组件开启了组件冻结,状态变量`@Local bookTest`将不响应更新,其@Monitor装饰的回调方法不会被调用,状态变量关联的组件不会刷新。 567- 点击`Button('Back')`回到前一个页面,调用@Monitor装饰的回调方法,状态变量关联的组件刷新。 568 569### 混用场景 570 571组件冻结混用场景即当支持组件冻结的场景彼此之间组合使用,对于不同的API version版本,冻结行为会有不同。给父组件设置组件冻结标志,在API version 17及以下,当父组件解冻时,会解冻自己子组件所有的节点;从API version 18开始,父组件解冻时,只会解冻子组件的屏上节点,详细说明见[\@Component的自定义组件冻结的混用场景](./arkts-custom-components-freeze.md#组件混用)。 572 573**Navigation和TabContent的混用** 574 575```ts 576@ComponentV2 577struct ChildOfParamComponent { 578 @Require @Param child_val: number; 579 580 @Monitor('child_val') onChange(m: IMonitor) { 581 console.info(`Appmonitor ChildOfParamComponent: changed ${m.dirty[0]}: ${m.value()?.before} -> ${m.value()?.now}`); 582 } 583 584 build() { 585 Column() { 586 Text(`Child Param: ${this.child_val}`); 587 } 588 } 589} 590 591@ComponentV2 592struct ParamComponent { 593 @Require @Param val: number; 594 595 @Monitor('val') onChange(m: IMonitor) { 596 console.info(`Appmonitor ParamComponent: changed ${m.dirty[0]}: ${m.value()?.before} -> ${m.value()?.now}`); 597 } 598 599 build() { 600 Column() { 601 Text(`val: ${this.val}`); 602 ChildOfParamComponent({child_val: this.val}); 603 } 604 } 605} 606 607@ComponentV2 608struct DelayComponent { 609 @Require @Param delayVal1: number; 610 611 @Monitor('delayVal1') onChange(m: IMonitor) { 612 console.info(`Appmonitor DelayComponent: changed ${m.dirty[0]}: ${m.value()?.before} -> ${m.value()?.now}`); 613 } 614 615 build() { 616 Column() { 617 Text(`Delay Param: ${this.delayVal1}`); 618 } 619 } 620} 621 622@ComponentV2 ({freezeWhenInactive: true}) 623struct TabsComponent { 624 private controller: TabsController = new TabsController(); 625 @Local tabState: number = 47; 626 627 @Monitor('tabState') onChange(m: IMonitor) { 628 console.info(`Appmonitor TabsComponent: changed ${m.dirty[0]}: ${m.value()?.before} -> ${m.value()?.now}`); 629 } 630 631 build() { 632 Column({space: 10}) { 633 Button(`Incr state ${this.tabState}`) 634 .fontSize(25) 635 .onClick(() => { 636 console.info('Button increment state value'); 637 this.tabState = this.tabState + 1; 638 }) 639 640 Tabs({ barPosition: BarPosition.Start, index: 0, controller: this.controller}) { 641 TabContent() { 642 ParamComponent({val: this.tabState}); 643 }.tabBar('Update') 644 TabContent() { 645 DelayComponent({delayVal1: this.tabState}); 646 }.tabBar('DelayUpdate') 647 } 648 .vertical(false) 649 .scrollable(true) 650 .barMode(BarMode.Fixed) 651 .barWidth(400).barHeight(150).animationDuration(400) 652 .width('100%') 653 .height(200) 654 .backgroundColor(0xF5F5F5) 655 } 656 } 657} 658 659@Entry 660@Component 661struct MyNavigationTestStack { 662 @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 663 664 @Builder 665 PageMap(name: string) { 666 if (name === 'pageOne') { 667 PageOneStack() 668 } else if (name === 'pageTwo') { 669 PageTwoStack() 670 } 671 } 672 673 build() { 674 Column() { 675 Navigation(this.pageInfo) { 676 Column() { 677 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 678 .width('80%') 679 .height(40) 680 .margin(20) 681 .onClick(() => { 682 this.pageInfo.pushPath({ name: 'pageOne' }); //将name指定的NavDestination页面信息入栈 683 }) 684 } 685 }.title('NavIndex') 686 .navDestination(this.PageMap) 687 .mode(NavigationMode.Stack) 688 } 689 } 690} 691 692@Component 693struct PageOneStack { 694 @Consume('pageInfo') pageInfo: NavPathStack; 695 696 build() { 697 NavDestination() { 698 Column() { 699 TabsComponent(); 700 701 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 702 .width('80%') 703 .height(40) 704 .margin(20) 705 .onClick(() => { 706 this.pageInfo.pushPathByName('pageTwo', null); 707 }) 708 }.width('100%').height('100%') 709 }.title('pageOne') 710 .onBackPressed(() => { 711 this.pageInfo.pop(); 712 return true; 713 }) 714 } 715} 716 717@Component 718struct PageTwoStack { 719 @Consume('pageInfo') pageInfo: NavPathStack; 720 721 build() { 722 NavDestination() { 723 Column() { 724 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 725 .width('80%') 726 .height(40) 727 .margin(20) 728 .onClick(() => { 729 this.pageInfo.pop(); 730 }) 731 }.width('100%').height('100%') 732 }.title('pageTwo') 733 .onBackPressed(() => { 734 this.pageInfo.pop(); 735 return true; 736 }) 737 } 738} 739``` 740 741在API version 17及以下: 742 743点击Next page进入下一个页面并返回,会解冻Tabcontent所有的标签。 744 745在API version 18及以上: 746 747点击Next page进入下一个页面并返回,只会解冻对应标签的节点。 748 749## 限制条件 750 751如下面的例子所示,FreezeBuildNode中使用了自定义节点[BuilderNode](../../reference/apis-arkui/js-apis-arkui-builderNode.md)。BuilderNode可以通过命令式动态挂载组件,而组件冻结又是强依赖父子关系来通知是否开启组件冻结。如果父组件使用组件冻结,且组件树的中间层级上又启用了BuilderNode,则BuilderNode的子组件将无法被冻结。 752 753``` 754import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI'; 755 756// 定义一个Params类,用于传递参数 757@ObservedV2 758class Params { 759 // 单例模式,确保只有一个Params实例 760 static singleton_: Params; 761 762 // 获取Params实例的方法 763 static instance() { 764 if (!Params.singleton_) { 765 Params.singleton_ = new Params(0); 766 } 767 return Params.singleton_; 768 } 769 770 // 使用@Trace装饰器装饰message属性,以便跟踪其变化 771 @Trace message: string = "Hello"; 772 index: number = 0; 773 774 constructor(index: number) { 775 this.index = index; 776 } 777} 778 779// 定义一个buildNodeChild组件,它包含一个message属性和一个index属性 780@ComponentV2 781struct buildNodeChild { 782 // 使用Params实例作为storage属性 783 storage: Params = Params.instance(); 784 @Param index: number = 0; 785 786 // 使用@Monitor装饰器监听storage.message的变化 787 @Monitor("storage.message") 788 onMessageChange(monitor: IMonitor) { 789 console.info(`FreezeBuildNode buildNodeChild message callback func ${this.storage.message}, index:${this.index}`); 790 } 791 792 build() { 793 Text(`buildNode Child message: ${this.storage.message}`).fontSize(30) 794 } 795} 796 797// 定义一个buildText函数,它接收一个Params参数并构建一个Column组件 798@Builder 799function buildText(params: Params) { 800 Column() { 801 buildNodeChild({ index: params.index }) 802 } 803} 804 805class TextNodeController extends NodeController { 806 private textNode: BuilderNode<[Params]> | null = null; 807 private index: number = 0; 808 809 // 构造函数接收一个index参数 810 constructor(index: number) { 811 super(); 812 this.index = index; 813 } 814 815 // 创建并返回一个FrameNode 816 makeNode(context: UIContext): FrameNode | null { 817 this.textNode = new BuilderNode(context); 818 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.index)); 819 return this.textNode.getFrameNode(); 820 } 821} 822 823// 定义一个Index组件,它包含一个message属性和一个data数组 824@Entry 825@ComponentV2 826struct Index { 827 // 使用Params实例作为storage属性 828 storage: Params = Params.instance(); 829 private data: number[] = [0, 1]; 830 831 build() { 832 Row() { 833 Column() { 834 Button("change").fontSize(30) 835 .onClick(() => { 836 this.storage.message += 'a'; 837 }) 838 839 Tabs() { 840 // 使用Repeat重复渲染TabContent组件 841 Repeat<number>(this.data) 842 .each((obj: RepeatItem<number>) => { 843 TabContent() { 844 FreezeBuildNode({ index: obj.item }) 845 .margin({ top: 20 }) 846 }.tabBar(`tab${obj.item}`) 847 }) 848 .key((item: number) => item.toString()) 849 } 850 } 851 } 852 .width('100%') 853 .height('100%') 854 } 855} 856 857// 定义一个FreezeBuildNode组件,它包含一个message属性和一个index属性 858@ComponentV2({ freezeWhenInactive: true }) 859struct FreezeBuildNode { 860 // 使用Params实例作为storage属性 861 storage: Params = Params.instance(); 862 @Param index: number = 0; 863 864 // 使用@Monitor装饰器监听storage.message的变化 865 @Monitor("storage.message") 866 onMessageChange(monitor: IMonitor) { 867 console.info(`FreezeBuildNode message callback func ${this.storage.message}, index: ${this.index}`); 868 } 869 870 build() { 871 NodeContainer(new TextNodeController(this.index)) 872 .width('100%') 873 .height('100%') 874 .backgroundColor('#FFF0F0F0') 875 } 876} 877``` 878 879点击Button("change")。改变message的值,当前正在显示的TabContent组件中的[@Watch](./arkts-watch.md)中注册的方法onMessageUpdated被触发。未显示的TabContent中的BuilderNode节点下组件的@Watch方法onMessageUpdated也被触发,并没有被冻结。 880 881