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