• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![freezeInPage](./figures/freezeInPage.png)
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![Example Image](./figures/freeze1.png)
102
1033. 点击“back”,页面2被销毁,页面1的状态由inactive变为active。状态变量“bookTest”的更新被观察到,@Monitor中注册的方法onMessageChange被调用,对应的Text显示内容改变。
104
105![freezeV2Page](./figures/freezeV2page.gif)
106
107### TabContent
108
109- 对Tabs中当前不可见的TabContent进行冻结,不会触发组件的更新。
110
111- 需要注意的是:在首次渲染的时候,Tab只会创建当前正在显示的TabContent,当切换全部的TabContent后,TabContent才会被全部创建。
112
113图示如下:
114![freezeWithTab](./figures/freezewithTabs.png)
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![TabContent.gif](figures/TabContent.gif)
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![navigation-freeze.gif](figures/navigation-freeze.gif)
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![freeze_repeat_L2.gif](figures/freeze_repeat_L2.gif)
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![freeze_repeat_L2_unfreeze.gif](figures/freeze_repeat_L2_unfreeze.gif)
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![builderNode.gif](figures/builderNode.gif)