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