• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# \@Reusable装饰器:组件复用
2
3
4\@Reusable装饰器装饰任意自定义组件时,表示该自定义组件可以复用。
5
6> **说明:**
7>
8> 从API version 10开始,对\@Reusable进行支持,支持在ArkTS中使用。
9
10
11
12## 概述
13
14- \@Reusable适用自定义组件,与\@Component结合使用,标记为\@Reusable的自定义组件从组件树上被移除时,组件和其对应的JSView对象都会被放入复用缓存中,后续创建新自定义组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。
15
16## 限制条件
17
18- \@Reusable装饰器仅用于自定义组件。
19
20```ts
21// 编译报错,仅用于自定义组件
22 @Reusable
23 @Builder
24 function buildCreativeLoadingDialog(closedClick: () => void) {
25   Crash()
26 }
27
28```
29
30- ComponentContent不支持传入\@Reusable装饰器装饰的自定义组件。
31
32```ts
33@Builder
34function buildCreativeLoadingDialog(closedClick: () => void) {
35  Crash()
36}
37
38// 如果注释掉就可以正常弹出弹窗,如果加上@Reusable就直接crash
39@Reusable
40@Component
41export struct Crash {
42  build() {
43    Column() {
44      Text("Crash")
45        .fontSize(12)
46        .lineHeight(18)
47        .fontColor(Color.Blue)
48        .margin({
49          left: 6
50        })
51    }.width('100%')
52    .height('100%')
53    .justifyContent(FlexAlign.Center)
54  }
55}
56
57@Entry
58@Component
59struct Index {
60  @State message: string = 'Hello World';
61  private uicontext = this.getUIContext()
62
63  build() {
64    RelativeContainer() {
65      Text(this.message)
66        .id('Index')
67        .fontSize(50)
68        .fontWeight(FontWeight.Bold)
69        .alignRules({
70          center: { anchor: '__container__', align: VerticalAlign.Center },
71          middle: { anchor: '__container__', align: HorizontalAlign.Center }
72        })
73        .onClick(() => {
74          // ComponentContent 底层时buildNode,buildNode不支持传入@Reusable注解的自定义组件
75          let contentNode = new ComponentContent(this.uicontext, wrapBuilder(buildCreativeLoadingDialog), () => {
76          });
77          this.uicontext.getPromptAction().openCustomDialog(contentNode);
78        })
79    }
80    .height('100%')
81    .width('100%')
82  }
83}
84```
85
86- \@Reusable装饰器不支持嵌套使用。
87
88```ts
89// Parent 被标记@Reusable
90@Reusable
91@Component
92export struct Parent{
93  build() {
94    Column() {
95      //  问题用法,编译不报错,有时显示正常,可能导致未定义的结果,不建议使用此用法
96      //  可复用的组件的子树中存在可复用的组件,可能导致未定义的结果
97      HasReusableChild()
98      Text("Parent")
99        .fontSize(12)
100        .lineHeight(18)
101        .fontColor(Color.Blue)
102        .margin({
103          left: 6
104        })
105    }.width('100%')
106    .height('100%')
107    .justifyContent(FlexAlign.Center)
108  }
109}
110
111//  子自定义组件被也被标记@Reusable
112@Reusable
113@Component
114export struct HasReusableChild {
115  build() {
116    Column() {
117      Text("hasReusableChild")
118        .fontSize(12)
119        .lineHeight(18)
120        .fontColor(Color.Blue)
121        .margin({
122          left: 6
123        })
124    }.width('100%')
125    .height('100%')
126    .justifyContent(FlexAlign.Center)
127  }
128}
129```
130
131## 使用场景
132
133- 列表滚动:当应用需要展示大量数据的列表,并且用户进行滚动操作时,频繁创建和销毁列表项的视图可能导致卡顿和性能问题。在这种情况下,使用列表组件的组件复用机制可以重用已经创建的列表项视图,提高滚动的流畅度。
134
135- 动态布局更新:如果应用中的界面需要频繁地进行布局更新,例如根据用户的操作或数据变化动态改变视图结构和样式,重复创建和销毁视图可能导致频繁的布局计算,影响帧率。在这种情况下,使用组件复用可以避免不必要的视图创建和布局计算,提高性能。
136
137- 频繁创建和销毁数据项的视图场景下。使用组件复用可以重用已创建的视图,只更新数据的内容,减少视图的创建和销毁,能有效提高性能。
138
139
140## 使用场景举例
141
142### 动态布局更新
143
144- 示例代码将Child自定义组件标记为复用组件,通过Button点击更新Child,触发Child复用;
145- \@Reusable:自定义组件被\@Reusable装饰器修饰,即表示其具备组件复用的能力;
146- aboutToReuse:当一个可复用的自定义组件从复用缓存中重新加入到节点树时,触发aboutToReuse生命周期回调,并将组件的构造参数传递给aboutToReuse;
147
148```ts
149// xxx.ets
150export class Message {
151  value: string | undefined;
152
153  constructor(value: string) {
154    this.value = value;
155  }
156}
157
158@Entry
159@Component
160struct Index {
161  @State switch: boolean = true;
162  build() {
163    Column() {
164      Button('Hello')
165        .fontSize(30)
166        .fontWeight(FontWeight.Bold)
167        .onClick(() => {
168          this.switch = !this.switch;
169        })
170      if (this.switch) {
171        Child({ message: new Message('Child') })
172          // 如果只有一个复用的组件,可以不用设置reuseId
173          .reuseId('Child')
174      }
175    }
176    .height("100%")
177    .width('100%')
178  }
179}
180
181@Reusable
182@Component
183struct Child {
184  @State message: Message = new Message('AboutToReuse');
185
186  aboutToReuse(params: Record<string, ESObject>) {
187    console.info("Recycle ====Child==");
188    this.message = params.message as Message;
189  }
190
191  build() {
192    Column() {
193      Text(this.message.value)
194        .fontSize(30)
195    }
196    .borderWidth(1)
197    .height(100)
198  }
199}
200```
201
202### 列表滚动配合LazyForEach使用
203
204- 示例代码将CardView自定义组件标记为复用组件,List上下滑动,触发CardView复用;
205- \@Reusable:自定义组件被@Reusable装饰器修饰,即表示其具备组件复用的能力;
206- 变量item的被\@State修饰,才能更新,非\@State修饰变量存在无法更新问题;
207
208```ts
209class MyDataSource implements IDataSource {
210  private dataArray: string[] = [];
211  private listener: DataChangeListener | undefined;
212
213  public totalCount(): number {
214    return this.dataArray.length;
215  }
216
217  public getData(index: number): string {
218    return this.dataArray[index];
219  }
220
221  public pushData(data: string): void {
222    this.dataArray.push(data);
223  }
224
225  public reloadListener(): void {
226    this.listener?.onDataReloaded();
227  }
228
229  public registerDataChangeListener(listener: DataChangeListener): void {
230    this.listener = listener;
231  }
232
233  public unregisterDataChangeListener(listener: DataChangeListener): void {
234    this.listener = undefined;
235  }
236}
237
238@Entry
239@Component
240struct ReuseDemo {
241  private data: MyDataSource = new MyDataSource();
242
243  // ...
244  build() {
245    Column() {
246      List() {
247        LazyForEach(this.data, (item: string) => {
248          ListItem() {
249            CardView({ item: item })
250          }
251        }, (item: string) => item)
252      }
253    }
254  }
255}
256
257// 复用组件
258@Reusable
259@Component
260export struct CardView {
261  @State item: string = '';
262
263  aboutToReuse(params: Record<string, Object>): void {
264    this.item = params.item as string;
265  }
266
267  build() {
268    Column() {
269      Text(this.item)
270        .fontSize(30)
271    }
272    .borderWidth(1)
273    .height(100)
274  }
275}
276```
277
278### if使用场景
279
280- 示例代码将OneMoment自定义组件标记为复用组件,List上下滑动,触发OneMoment复用;
281- 可以使用reuseId为复用组件分配复用组,相同reuseId的组件会在同一个复用组中复用,如果只有一个复用的组件,可以不用设置reuseId;
282- 通过reuseId来标识需要复用的组件,省去重复执行if的删除重创逻辑,提高组件复用的效率和性能;
283
284```ts
285@Entry
286@Component
287struct withoutReuseId {
288  aboutToAppear(): void {
289    getFriendMomentFromRawfile();
290  }
291
292  build() {
293    Column() {
294      TopBar()
295      List({ space: ListConstants.LIST_SPACE }) {
296        LazyForEach(momentData, (moment: FriendMoment) => {
297          ListItem() {
298            OneMoment({moment: moment})
299              // 使用reuseId进行组件复用的控制
300              .reuseId((moment.image !== '') ? 'withImage' : 'noImage')
301          }
302        }, (moment: FriendMoment) => moment.id)
303      }
304      .cachedCount(Constants.CACHED_COUNT)
305    }
306  }
307}
308
309@Reusable
310@Component
311export struct OneMoment {
312  @Prop moment: FriendMoment;
313
314  build() {
315    Column() {
316      ...
317      Text(this.moment.text)
318
319      if (this.moment.image !== '') {
320        Flex({ wrap: FlexWrap.Wrap }) {
321          Image($r(this.moment.image))
322          Image($r(this.moment.image))
323          Image($r(this.moment.image))
324          Image($r(this.moment.image))
325        }
326      }
327      ...
328    }
329  }
330}
331```
332
333### foreach使用场景
334
335- 示例点击update,数据刷新成功,但是滑动列表,组件复用无法使用,foreach的折叠展开属性的原因;
336- 点击clear,再次update,复用成功;符合一帧内重复创建多个已被销毁的自定义组件;
337
338```ts
339// xxx.ets
340class MyDataSource implements IDataSource {
341  private dataArray: string[] = [];
342
343  public totalCount(): number {
344    return this.dataArray.length;
345  }
346
347  public getData(index: number): string {
348    return this.dataArray[index];
349  }
350
351  public pushData(data: string): void {
352    this.dataArray.push(data);
353  }
354
355  public registerDataChangeListener(listener: DataChangeListener): void {
356  }
357
358  public unregisterDataChangeListener(listener: DataChangeListener): void {
359  }
360}
361
362@Entry
363@Component
364struct Index {
365  private data: MyDataSource = new MyDataSource();
366  private data02: MyDataSource = new MyDataSource();
367  @State isShow: boolean = true
368  @State dataSource: ListItemObject[] = [];
369
370  aboutToAppear() {
371    for (let i = 0; i < 100; i++) {
372      this.data.pushData(i.toString())
373    }
374
375    for (let i = 30; i < 80; i++) {
376      this.data02.pushData(i.toString())
377    }
378  }
379
380  build() {
381    Column() {
382      Row() {
383        Button('clear').onClick(() => {
384          for (let i = 1; i < 50; i++) {
385            let obj = new ListItemObject();
386            obj.id = i;
387            obj.uuid = Math.random().toString();
388            obj.isExpand = false
389            this.dataSource.pop();
390          }
391        }).height(40)
392
393        Button('update').onClick(() => {
394          for (let i = 1; i < 50; i++) {
395            let obj = new ListItemObject();
396            obj.id = i;
397            obj.uuid = Math.random().toString();
398            obj.isExpand = false
399            this.dataSource.push(obj);
400          }
401        }).height(40)
402      }
403
404      List({ space: 10 }) {
405        ForEach(this.dataSource, (item: ListItemObject) => {
406          ListItem() {
407            ListItemView({
408              obj: item
409            })
410          }
411        }, (item: ListItemObject) => {
412          return item.uuid.toString()
413        })
414
415      }.cachedCount(0)
416      .width('100%')
417      .height('100%')
418    }
419  }
420}
421
422@Reusable
423@Component
424struct ListItemView {
425  @ObjectLink obj: ListItemObject;
426  @State item: string = ''
427
428  aboutToAppear(): void {
429    // 点击 update,首次进入,上下滑动,由于foreach折叠展开属性,无法复用
430    console.log("=====abouTo===Appear=====ListItemView==创建了==" + this.item)
431  }
432
433  aboutToReuse(params: ESObject) {
434    this.item = params.item;
435    // 点击 clear,再次update ,复用成功
436    //符合一帧内重复创建多个已被销毁的自定义组件
437    console.log("=====aboutTo===Reuse====ListItemView==复用了==" + this.item)
438  }
439
440  build() {
441    Column({ space: 10 }) {
442      Text(`${this.obj.id}.标题`)
443        .fontSize(16)
444        .fontColor('#000000')
445        .padding({
446          top: 20,
447          bottom: 20,
448        })
449
450      if (this.obj.isExpand) {
451        Text('')
452          .fontSize(14)
453          .fontColor('#999999')
454      }
455    }
456    .width('100%')
457    .borderRadius(10)
458    .backgroundColor(Color.White)
459    .padding(15)
460    .onClick(() => {
461      this.obj.isExpand = !this.obj.isExpand;
462    })
463  }
464}
465
466@Observed
467class ListItemObject {
468  uuid: string = ""
469  id: number = 0;
470  isExpand: boolean = false;
471}
472```
473
474### Grid使用场景
475
476- 示例中使用\@Reusable装饰器修饰GridItem中的自定义组件ReusableChildComponent,即表示其具备组件复用的能力;
477- 使用aboutToReuse是为了让Grid在滑动时从复用缓存中加入到组件树之前触发,用于更新组件的状态变量以展示正确的内容;
478- 需要注意的是无需在aboutToReuse中对\@Link、\@StorageLink、\@ObjectLink、\@Consume等自动更新值的状态变量进行更新,可能触发不必要的组件刷新。
479
480```ts
481// MyDataSource类实现IDataSource接口
482class MyDataSource implements IDataSource {
483  private dataArray: number[] = [];
484
485  public pushData(data: number): void {
486    this.dataArray.push(data);
487  }
488
489  // 数据源的数据总量
490  public totalCount(): number {
491    return this.dataArray.length;
492  }
493
494  // 返回指定索引位置的数据
495  public getData(index: number): number {
496    return this.dataArray[index];
497  }
498
499  registerDataChangeListener(listener: DataChangeListener): void {
500  }
501
502  unregisterDataChangeListener(listener: DataChangeListener): void {
503  }
504}
505
506@Entry
507@Component
508struct MyComponent {
509  // 数据源
510  private data: MyDataSource = new MyDataSource();
511
512  aboutToAppear() {
513    for (let i = 1; i < 1000; i++) {
514      this.data.pushData(i);
515    }
516  }
517
518  build() {
519    Column({ space: 5 }) {
520      Grid() {
521        LazyForEach(this.data, (item: number) => {
522          GridItem() {
523            // 使用可复用自定义组件
524            ReusableChildComponent({ item: item })
525          }
526        }, (item: string) => item)
527      }
528      .cachedCount(2) // 设置GridItem的缓存数量
529      .columnsTemplate('1fr 1fr 1fr')
530      .columnsGap(10)
531      .rowsGap(10)
532      .margin(10)
533      .height(500)
534      .backgroundColor(0xFAEEE0)
535    }
536  }
537}
538
539// 自定义组件被@Reusable装饰器修饰,即标志其具备组件复用的能力
540@Reusable
541@Component
542struct ReusableChildComponent {
543  @State item: number = 0;
544
545  // aboutToReuse从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容
546  // aboutToReuse参数类型已不支持any,这里使用Record指定明确的数据类型。Record用于构造一个对象类型,其属性键为Keys,属性值为Type
547  aboutToReuse(params: Record<string, number>) {
548    this.item = params.item;
549  }
550
551  build() {
552    Column() {
553      Image($r('app.media.icon'))
554        .objectFit(ImageFit.Fill)
555        .layoutWeight(1)
556      Text(`图片${this.item}`)
557        .fontSize(16)
558        .textAlign(TextAlign.Center)
559    }
560    .width('100%')
561    .height(120)
562    .backgroundColor(0xF9CF93)
563  }
564}
565```
566
567### WaterFlow使用场景
568
569- WaterFlow滑动场景存在FlowItem及其子组件的频繁创建和销毁,可以将FlowItem中的组件封装成自定义组件,并使用\@Reusable装饰器修饰,使其具备组件复用能力;
570
571```ts
572  build() {
573    Column({ space: 2 }) {
574      WaterFlow() {
575        LazyForEach(this.datasource, (item: number) => {
576          FlowItem() {
577            // 使用可复用自定义组件
578            ReusableFlowItem({ item: item })
579          }
580          .onAppear(() => {
581            // 即将触底时提前增加数据
582            if (item + 20 == this.datasource.totalCount()) {
583              for (let i = 0; i < 100; i++) {
584                this.datasource.AddLastItem()
585              }
586            }
587          })
588          .width('100%')
589          .height(this.itemHeightArray[item % 100])
590          .backgroundColor(this.colors[item % 5])
591        }, (item: string) => item)
592      }
593      .columnsTemplate("1fr 1fr")
594      .columnsGap(10)
595      .rowsGap(5)
596      .backgroundColor(0xFAEEE0)
597      .width('100%')
598      .height('80%')
599    }
600  }
601@Reusable
602@Component
603struct ReusableFlowItem {
604  @State item: number = 0
605
606  // 从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容
607  aboutToReuse(params) {
608    this.item = params.item;
609  }
610
611  build() {
612    Column() {
613      Text("N" + this.item).fontSize(12).height('16')
614      Image('res/waterFlowTest (' + this.item % 5 + ').jpg')
615        .objectFit(ImageFit.Fill)
616        .width('100%')
617        .layoutWeight(1)
618    }
619  }
620}
621```
622
623### 多种条目类型使用场景
624
625#### 标准型
626
627- 复用组件之间布局完全相同;
628- 示例同列表滚动中描述;
629
630#### 有限变化型
631
632- 复用组件之间有不同,但是类型有限;
633- 示例为复用组件显式设置两个reuseId与使用两个自定义组件进行复用;
634
635```ts
636class MyDataSource implements IDataSource {
637  ...
638}
639
640@Entry
641@Component
642struct Index {
643  private data: MyDataSource = new MyDataSource();
644
645  aboutToAppear() {
646    for (let i = 0; i < 1000; i++) {
647      this.data.pushData(i);
648    }
649  }
650
651  build() {
652    Column() {
653      List({ space: 10 }) {
654        LazyForEach(this.data, (item: number) => {
655          ListItem() {
656            ReusableComponent({ item: item })
657              .reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo')
658          }
659          .backgroundColor(Color.Orange)
660          .width('100%')
661        }, (item: number) => item.toString())
662      }
663      .cachedCount(2)
664    }
665  }
666}
667
668@Reusable
669@Component
670struct ReusableComponent {
671  @State item: number = 0;
672
673  aboutToReuse(params: ESObject) {
674    this.item = params.item;
675  }
676
677  build() {
678    Column() {
679      if (this.item % 2 === 0) {
680        Text(`Item ${this.item} ReusableComponentOne`)
681          .fontSize(20)
682          .margin({ left: 10 })
683      } else {
684        Text(`Item ${this.item} ReusableComponentTwo`)
685          .fontSize(20)
686          .margin({ left: 10 })
687      }
688    }.margin({ left: 10, right: 10 })
689  }
690}
691
692```
693
694#### 组合型
695
696- 复用组件之间有不同,情况非常多,但是拥有共同的子组件;
697- 示例按照组合型的组件复用方式,将上述示例中的三种复用组件转变为Builder函数后,内部共同的子组件就处于同一个父组件MyComponent下;
698- 对这些子组件使用组件复用时,它们的缓存池也会在父组件上共享,节省组件创建时的消耗。
699
700```ts
701class MyDataSource implements IDataSource {
702  ...
703}
704
705@Entry
706@Component
707struct MyComponent {
708  private data: MyDataSource = new MyDataSource();
709
710  aboutToAppear() {
711    for (let i = 0; i < 1000; i++) {
712      this.data.pushData(i.toString())
713    }
714  }
715
716  @Builder
717  itemBuilderOne(item: string) {
718    Column() {
719      ChildComponentA({ item: item })
720      ChildComponentB({ item: item })
721      ChildComponentC({ item: item })
722    }
723  }
724
725  @Builder
726  itemBuilderTwo(item: string) {
727    Column() {
728      ChildComponentA({ item: item })
729      ChildComponentC({ item: item })
730      ChildComponentD({ item: item })
731    }
732  }
733
734  @Builder
735  itemBuilderThree(item: string) {
736    Column() {
737      ChildComponentA({ item: item })
738      ChildComponentB({ item: item })
739      ChildComponentD({ item: item })
740    }
741  }
742
743  build() {
744    List({ space: 40 }) {
745      LazyForEach(this.data, (item: string, index: number) => {
746        ListItem() {
747          if (index % 3 === 0) {
748            this.itemBuilderOne(item)
749          } else if (index % 5 === 0) {
750            this.itemBuilderTwo(item)
751          } else {
752            this.itemBuilderThree(item)
753          }
754        }
755        .backgroundColor('#cccccc')
756        .width('100%')
757        .onAppear(() => {
758          console.log(`ListItem ${index} onAppear`);
759        })
760      }, (item: number) => item.toString())
761    }
762    .width('100%')
763    .height('100%')
764    .cachedCount(0)
765  }
766}
767
768@Reusable
769@Component
770struct ChildComponentA {
771  @State item: string = '';
772
773  aboutToReuse(params: ESObject) {
774    console.log(`ChildComponentA ${params.item} Reuse ${this.item}`);
775    this.item = params.item;
776  }
777
778  aboutToRecycle(): void {
779    console.log(`ChildComponentA ${this.item} Recycle`);
780  }
781
782  build() {
783    Column() {
784      Text(`Item ${this.item} Child Component A`)
785        .fontSize(20)
786        .margin({ left: 10 })
787        .fontColor(Color.Blue)
788      Grid() {
789        ForEach((new Array(20)).fill(''), (item: string,index: number) => {
790          GridItem() {
791            Image($r('app.media.startIcon'))
792              .height(20)
793          }
794        })
795      }
796      .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
797      .rowsTemplate('1fr 1fr 1fr 1fr')
798      .columnsGap(10)
799      .width('90%')
800      .height(160)
801    }
802    .margin({ left: 10, right: 10 })
803    .backgroundColor(0xFAEEE0)
804  }
805}
806
807@Reusable
808@Component
809struct ChildComponentB {
810  @State item: string = '';
811
812  aboutToReuse(params: ESObject) {
813    this.item = params.item;
814  }
815
816  build() {
817    Row() {
818      Text(`Item ${this.item} Child Component B`)
819        .fontSize(20)
820        .margin({ left: 10 })
821        .fontColor(Color.Red)
822    }.margin({ left: 10, right: 10 })
823  }
824}
825
826@Reusable
827@Component
828struct ChildComponentC {
829  @State item: string = '';
830
831  aboutToReuse(params: ESObject) {
832    this.item = params.item;
833  }
834
835  build() {
836    Row() {
837      Text(`Item ${this.item} Child Component C`)
838        .fontSize(20)
839        .margin({ left: 10 })
840        .fontColor(Color.Green)
841    }.margin({ left: 10, right: 10 })
842  }
843}
844
845@Reusable
846@Component
847struct ChildComponentD {
848  @State item: string = '';
849
850  aboutToReuse(params: ESObject) {
851    this.item = params.item;
852  }
853
854  build() {
855    Row() {
856      Text(`Item ${this.item} Child Component D`)
857        .fontSize(20)
858        .margin({ left: 10 })
859        .fontColor(Color.Orange)
860    }.margin({ left: 10, right: 10 })
861  }
862}
863```
864