• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# \@Reusable装饰器:组件复用
2<!--Kit: ArkUI-->
3<!--Subsystem: ArkUI-->
4<!--Owner: @liyujie43-->
5<!--Designer: @lizhan-->
6<!--Tester: @TerryTsao-->
7<!--Adviser: @zhang_yixin13-->
8
9\@Reusable装饰器标记的自定义组件支持视图节点、组件实例和状态上下文的复用,避免重复创建和销毁,提升性能。
10
11## 概述
12
13使用\@Reusable装饰器时,表示该自定义组件可以复用。与[\@Component装饰器](arkts-create-custom-components.md#component)结合使用,标记为\@Reusable的自定义组件在从组件树中移除时,组件及其对应的JS对象将被放入复用缓存中。后续创建新自定义组件节点时,将复用缓存中的节点,从而节约组件重新创建的时间。
14
15> **说明:**
16>
17> API version 10开始支持@Reusable,支持在ArkTS中使用。
18>
19> 关于组件复用的原理与使用、优化方法、适用场景,请参考最佳实践[组件复用最佳实践](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-component-reuse)20>
21> \@Reusable标识之后,在组件上下树时ArkUI框架会调用该组件的[aboutToReuse](../../../application-dev/reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#abouttoreuse10)方法和[aboutToRecycle](../../../application-dev/reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#abouttorecycle10)方法,因此,开发者在实现复用时,大部分代码都集中在这两个生命周期方法中。
22>
23> 如果一个组件里可复用的组件不止一个,可以使用[reuseId](../../../application-dev/reference/apis-arkui/arkui-ts/ts-universal-attributes-reuse-id.md)来区分不同结构的复用组件。
24>
25
26## 限制条件
27
28- \@Reusable装饰器仅用于自定义组件。
29
30```ts
31import { ComponentContent } from "@kit.ArkUI";
32
33// @Builder加上@Reusable编译报错,不适用于builder。
34// @Reusable
35@Builder
36function buildCreativeLoadingDialog(closedClick: () => void) {
37  Crash()
38}
39
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          let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => {
75          });
76          this.uiContext.getPromptAction().openCustomDialog(contentNode);
77        })
78    }
79    .height('100%')
80    .width('100%')
81  }
82}
83```
84
85- 被@Reusable装饰的自定义组件在复用时,会递归调用该自定义组件及其所有子组件的aboutToReuse回调函数。若在子组件的aboutToReuse函数中修改了父组件的状态变量,此次修改将不会生效,请避免此类用法。若需设置父组件的状态变量,可使用setTimeout设置延迟执行,将任务抛出组件复用的作用范围,使修改生效。
86
87
88  【反例】
89
90  在子组件的aboutToReuse中,直接修改父组件的状态变量。
91
92  ```ts
93  class BasicDataSource implements IDataSource {
94    private listener: DataChangeListener | undefined = undefined;
95    public dataArray: number[] = [];
96
97    totalCount(): number {
98      return this.dataArray.length;
99    }
100
101    getData(index: number): number {
102      return this.dataArray[index];
103    }
104
105    registerDataChangeListener(listener: DataChangeListener): void {
106      this.listener = listener;
107    }
108
109    unregisterDataChangeListener(listener: DataChangeListener): void {
110      this.listener = undefined;
111    }
112  }
113
114  @Entry
115  @Component
116  struct Index {
117    private data: BasicDataSource = new BasicDataSource();
118
119    aboutToAppear(): void {
120      for (let index = 1; index < 20; index++) {
121        this.data.dataArray.push(index);
122      }
123    }
124
125    build() {
126      List() {
127        LazyForEach(this.data, (item: number, index: number) => {
128          ListItem() {
129            ReuseComponent({ num: item })
130          }
131        }, (item: number, index: number) => index.toString())
132      }.cachedCount(0)
133    }
134  }
135
136  @Reusable
137  @Component
138  struct ReuseComponent {
139    @State num: number = 0;
140
141    aboutToReuse(params: ESObject): void {
142      this.num = params.num;
143    }
144
145    build() {
146      Column() {
147        Text('ReuseComponent num:' + this.num.toString())
148        ReuseComponentChild({ num: this.num })
149        Button('plus')
150          .onClick(() => {
151            this.num += 10;
152          })
153      }
154      .height(200)
155    }
156  }
157
158  @Component
159  struct ReuseComponentChild {
160    @Link num: number;
161
162    aboutToReuse(params: ESObject): void {
163      this.num = -1 * params.num;
164    }
165
166    build() {
167      Text('ReuseComponentChild num:' + this.num.toString())
168    }
169  }
170  ```
171
172  【正例】
173
174  在子组件的aboutToReuse中,使用setTimeout,将修改抛出组件复用的作用范围。
175
176  ```ts
177  class BasicDataSource implements IDataSource {
178    private listener: DataChangeListener | undefined = undefined;
179    public dataArray: number[] = [];
180
181    totalCount(): number {
182      return this.dataArray.length;
183    }
184
185    getData(index: number): number {
186      return this.dataArray[index];
187    }
188
189    registerDataChangeListener(listener: DataChangeListener): void {
190      this.listener = listener;
191    }
192
193    unregisterDataChangeListener(listener: DataChangeListener): void {
194      this.listener = undefined;
195    }
196  }
197
198  @Entry
199  @Component
200  struct Index {
201    private data: BasicDataSource = new BasicDataSource();
202
203    aboutToAppear(): void {
204      for (let index = 1; index < 20; index++) {
205        this.data.dataArray.push(index);
206      }
207    }
208
209    build() {
210      List() {
211        LazyForEach(this.data, (item: number, index: number) => {
212          ListItem() {
213            ReuseComponent({ num: item })
214          }
215        }, (item: number, index: number) => index.toString())
216      }.cachedCount(0)
217    }
218  }
219
220  @Reusable
221  @Component
222  struct ReuseComponent {
223    @State num: number = 0;
224
225    aboutToReuse(params: ESObject): void {
226      this.num = params.num;
227    }
228
229    build() {
230      Column() {
231        Text('ReuseComponent num:' + this.num.toString())
232        ReuseComponentChild({ num: this.num })
233        Button('plus')
234          .onClick(() => {
235            this.num += 10;
236          })
237      }
238      .height(200)
239    }
240  }
241
242  @Component
243  struct ReuseComponentChild {
244    @Link num: number;
245
246    aboutToReuse(params: ESObject): void {
247      setTimeout(() => {
248        this.num = -1 * params.num;
249      }, 1)
250    }
251
252    build() {
253      Text('ReuseComponentChild num:' + this.num.toString())
254    }
255  }
256  ```
257
258- ComponentContent不支持传入\@Reusable装饰器装饰的自定义组件。
259
260```ts
261import { ComponentContent } from "@kit.ArkUI";
262
263@Builder
264function buildCreativeLoadingDialog(closedClick: () => void) {
265  Crash()
266}
267
268// 如果注释掉就可以正常弹出弹窗,如果加上@Reusable就直接crash。
269@Reusable
270@Component
271export struct Crash {
272  build() {
273    Column() {
274      Text("Crash")
275        .fontSize(12)
276        .lineHeight(18)
277        .fontColor(Color.Blue)
278        .margin({
279          left: 6
280        })
281    }.width('100%')
282    .height('100%')
283    .justifyContent(FlexAlign.Center)
284  }
285}
286
287@Entry
288@Component
289struct Index {
290  @State message: string = 'Hello World';
291  private uiContext = this.getUIContext();
292
293  build() {
294    RelativeContainer() {
295      Text(this.message)
296        .id('Index')
297        .fontSize(50)
298        .fontWeight(FontWeight.Bold)
299        .alignRules({
300          center: { anchor: '__container__', align: VerticalAlign.Center },
301          middle: { anchor: '__container__', align: HorizontalAlign.Center }
302        })
303        .onClick(() => {
304          // ComponentContent底层是BuilderNode,BuilderNode不支持传入@Reusable注解的自定义组件。
305          let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => {
306          });
307          this.uiContext.getPromptAction().openCustomDialog(contentNode);
308        })
309    }
310    .height('100%')
311    .width('100%')
312  }
313}
314```
315
316- \@Reusable装饰器不建议嵌套使用,会增加内存,降低复用效率,加大维护难度。嵌套使用会导致额外缓存池的生成,各缓存池拥有相同树状结构,复用效率低下。此外,嵌套使用会使生命周期管理复杂,资源和变量共享困难。
317
318
319## 使用场景
320
321### 动态布局更新
322
323重复创建与移除视图可能引起频繁的布局计算,从而影响帧率。采用组件复用可以避免不必要的视图创建与布局计算,提升性能。
324以下示例中,将Child自定义组件标记为复用组件,通过Button点击更新Child,触发复用。
325
326```ts
327// xxx.ets
328export class Message {
329  value: string | undefined;
330
331  constructor(value: string) {
332    this.value = value;
333  }
334}
335
336@Entry
337@Component
338struct Index {
339  @State switch: boolean = true;
340
341  build() {
342    Column() {
343      Button('Hello')
344        .fontSize(30)
345        .fontWeight(FontWeight.Bold)
346        .onClick(() => {
347          this.switch = !this.switch;
348        })
349      if (this.switch) {
350        // 如果只有一个复用的组件,可以不用设置reuseId。
351        Child({ message: new Message('Child') })
352          .reuseId('Child')
353      }
354    }
355    .height("100%")
356    .width('100%')
357  }
358}
359
360@Reusable
361@Component
362struct Child {
363  @State message: Message = new Message('AboutToReuse');
364
365  aboutToReuse(params: Record<string, ESObject>) {
366    console.info("Recycle====Child==");
367    this.message = params.message as Message;
368  }
369
370  build() {
371    Column() {
372      Text(this.message.value)
373        .fontSize(30)
374    }
375    .borderWidth(1)
376    .height(100)
377  }
378}
379```
380
381### 列表滚动配合LazyForEach使用
382
383- 当应用展示大量数据的列表并进行滚动操作时,频繁创建和销毁列表项视图可能导致卡顿和性能问题。使用列表组件的组件复用机制可以重用已创建的列表项视图,提高滚动流畅度。
384
385- 以下示例代码将CardView自定义组件标记为复用组件,List上下滑动,触发CardView复用。
386
387```ts
388class MyDataSource implements IDataSource {
389  private dataArray: string[] = [];
390  private listener: DataChangeListener | undefined;
391
392  public totalCount(): number {
393    return this.dataArray.length;
394  }
395
396  public getData(index: number): string {
397    return this.dataArray[index];
398  }
399
400  public pushData(data: string): void {
401    this.dataArray.push(data);
402  }
403
404  public reloadListener(): void {
405    this.listener?.onDataReloaded();
406  }
407
408  public registerDataChangeListener(listener: DataChangeListener): void {
409    this.listener = listener;
410  }
411
412  public unregisterDataChangeListener(listener: DataChangeListener): void {
413    this.listener = undefined;
414  }
415}
416
417@Entry
418@Component
419struct ReuseDemo {
420  private data: MyDataSource = new MyDataSource();
421
422  aboutToAppear() {
423    for (let i = 1; i < 1000; i++) {
424      this.data.pushData(i + "");
425    }
426  }
427
428  // ...
429  build() {
430    Column() {
431      List() {
432        LazyForEach(this.data, (item: string) => {
433          ListItem() {
434            CardView({ item: item })
435          }
436        }, (item: string) => item)
437      }
438    }
439  }
440}
441
442// 复用组件
443@Reusable
444@Component
445export struct CardView {
446  // 被\@State修饰的变量item才能更新,未被\@State修饰的变量不会更新。
447  @State item: string = '';
448
449  aboutToReuse(params: Record<string, Object>): void {
450    this.item = params.item as string;
451  }
452
453  build() {
454    Column() {
455      Text(this.item)
456        .fontSize(30)
457    }
458    .borderWidth(1)
459    .height(100)
460  }
461}
462```
463
464### 列表滚动-if使用场景
465
466以下示例代码将OneMoment自定义组件标记为复用组件。当List上下滑动时,会触发OneMoment的复用。设置reuseId可为复用组件分配复用组,相同reuseId的组件将在同一复用组中复用。单个复用组件无需设置reuseId。使用reuseId标识复用组件,可避免重复执行if语句的删除和重新创建逻辑,提高复用效率和性能。
467
468```ts
469@Entry
470@Component
471struct Index {
472  private dataSource = new MyDataSource<FriendMoment>();
473
474  aboutToAppear(): void {
475    for (let i = 0; i < 20; i++) {
476      let title = i + 1 + "test_if";
477      this.dataSource.pushData(new FriendMoment(i.toString(), title, 'app.media.app_icon'));
478    }
479
480    for (let i = 0; i < 50; i++) {
481      let title = i + 1 + "test_if";
482      this.dataSource.pushData(new FriendMoment(i.toString(), title, ''));
483    }
484  }
485
486  build() {
487    Column() {
488      // TopBar()
489      List({ space: 3 }) {
490        LazyForEach(this.dataSource, (moment: FriendMoment) => {
491          ListItem() {
492            // 使用reuseId进行组件复用的控制。
493            OneMoment({ moment: moment })
494              .reuseId((moment.image !== '') ? 'withImage' : 'noImage')
495          }
496        }, (moment: FriendMoment) => moment.id)
497      }
498      .cachedCount(0)
499    }
500  }
501}
502
503class FriendMoment {
504  id: string = '';
505  text: string = '';
506  title: string = '';
507  image: string = '';
508  answers: Array<ResourceStr> = [];
509
510  constructor(id: string, title: string, image: string) {
511    this.text = id;
512    this.title = title;
513    this.image = image;
514  }
515}
516
517@Reusable
518@Component
519export struct OneMoment {
520  @Prop moment: FriendMoment;
521
522  // 复用id相同的组件才能触发复用。
523  aboutToReuse(params: ESObject): void {
524    console.log("=====aboutToReuse====OneMoment==复用了==" + this.moment.text);
525  }
526
527  build() {
528    Column() {
529      Text(this.moment.text)
530      // if分支判断。
531      if (this.moment.image !== '') {
532        Flex({ wrap: FlexWrap.Wrap }) {
533          Image($r(this.moment.image)).height(50).width(50)
534          Image($r(this.moment.image)).height(50).width(50)
535          Image($r(this.moment.image)).height(50).width(50)
536          Image($r(this.moment.image)).height(50).width(50)
537        }
538      }
539    }
540  }
541}
542
543class BasicDataSource<T> implements IDataSource {
544  private listeners: DataChangeListener[] = [];
545  private originDataArray: T[] = [];
546
547  public totalCount(): number {
548    return 0;
549  }
550
551  public getData(index: number): T {
552    return this.originDataArray[index];
553  }
554
555  registerDataChangeListener(listener: DataChangeListener): void {
556    if (this.listeners.indexOf(listener) < 0) {
557      this.listeners.push(listener);
558    }
559  }
560
561  unregisterDataChangeListener(listener: DataChangeListener): void {
562    const pos = this.listeners.indexOf(listener);
563    if (pos >= 0) {
564      this.listeners.splice(pos, 1);
565    }
566  }
567
568  notifyDataAdd(index: number): void {
569    this.listeners.forEach(listener => {
570      listener.onDataAdd(index);
571    });
572  }
573}
574
575export class MyDataSource<T> extends BasicDataSource<T> {
576  private dataArray: T[] = [];
577
578  public totalCount(): number {
579    return this.dataArray.length;
580  }
581
582  public getData(index: number): T {
583    return this.dataArray[index];
584  }
585
586  public pushData(data: T): void {
587    this.dataArray.push(data);
588    this.notifyDataAdd(this.dataArray.length - 1);
589  }
590}
591```
592
593### 列表滚动-Foreach使用场景
594
595使用Foreach创建可复用的自定义组件,由于Foreach渲染控制语法的全展开属性,导致复用组件无法复用。示例中点击update,数据刷新成功,但滑动列表时,ListItemView无法复用。点击clear,再次点击update,ListItemView复用成功,因为一帧内重复创建多个已被销毁的自定义组件。
596
597```ts
598// xxx.ets
599class MyDataSource implements IDataSource {
600  private dataArray: string[] = [];
601
602  public totalCount(): number {
603    return this.dataArray.length;
604  }
605
606  public getData(index: number): string {
607    return this.dataArray[index];
608  }
609
610  public pushData(data: string): void {
611    this.dataArray.push(data);
612  }
613
614  public registerDataChangeListener(listener: DataChangeListener): void {
615  }
616
617  public unregisterDataChangeListener(listener: DataChangeListener): void {
618  }
619}
620
621@Entry
622@Component
623struct Index {
624  private data: MyDataSource = new MyDataSource();
625  private data02: MyDataSource = new MyDataSource();
626  @State isShow: boolean = true;
627  @State dataSource: ListItemObject[] = [];
628
629  aboutToAppear() {
630    for (let i = 0; i < 100; i++) {
631      this.data.pushData(i.toString());
632    }
633
634    for (let i = 30; i < 80; i++) {
635      this.data02.pushData(i.toString());
636    }
637  }
638
639  build() {
640    Column() {
641      Row() {
642        Button('clear').onClick(() => {
643          for (let i = 1; i < 50; i++) {
644            this.dataSource.pop();
645          }
646        }).height(40)
647
648        Button('update').onClick(() => {
649          for (let i = 1; i < 50; i++) {
650            let obj = new ListItemObject();
651            obj.id = i;
652            obj.uuid = Math.random().toString();
653            obj.isExpand = false;
654            this.dataSource.push(obj);
655          }
656        }).height(40)
657      }
658
659      List({ space: 10 }) {
660        ForEach(this.dataSource, (item: ListItemObject) => {
661          ListItem() {
662            ListItemView({
663              obj: item
664            })
665          }
666        }, (item: ListItemObject) => {
667          return item.uuid.toString();
668        })
669
670      }.cachedCount(0)
671      .width('100%')
672      .height('100%')
673    }
674  }
675}
676
677@Reusable
678@Component
679struct ListItemView {
680  @ObjectLink obj: ListItemObject;
681  @State item: string = '';
682
683  aboutToAppear(): void {
684    // 点击 update,首次进入,上下滑动,由于Foreach折叠展开属性,无法复用。
685    console.log("=====aboutToAppear=====ListItemView==创建了==" + this.item);
686  }
687
688  aboutToReuse(params: ESObject) {
689    this.item = params.item;
690    // 点击clear,再次update,复用成功。
691    // 符合一帧内重复创建多个已被销毁的自定义组件。
692    console.log("=====aboutToReuse====ListItemView==复用了==" + this.item);
693  }
694
695  build() {
696    Column({ space: 10 }) {
697      Text(`${this.obj.id}.标题`)
698        .fontSize(16)
699        .fontColor('#000000')
700        .padding({
701          top: 20,
702          bottom: 20,
703        })
704
705      if (this.obj.isExpand) {
706        Text('')
707          .fontSize(14)
708          .fontColor('#999999')
709      }
710    }
711    .width('100%')
712    .borderRadius(10)
713    .backgroundColor(Color.White)
714    .padding(15)
715    .onClick(() => {
716      this.obj.isExpand = !this.obj.isExpand;
717    })
718  }
719}
720
721@Observed
722class ListItemObject {
723  uuid: string = "";
724  id: number = 0;
725  isExpand: boolean = false;
726}
727```
728
729### Grid使用场景
730
731示例中使用\@Reusable装饰器修饰GridItem中的自定义组件ReusableChildComponent,即表示其具备组件复用的能力。
732使用aboutToReuse可以在 Grid 滑动时,从复用缓存中加入到组件树之前触发,从而更新组件状态变量,展示正确内容。
733需要注意的是无需在aboutToReuse中对[\@Link](arkts-link.md)、[\@StorageLink](arkts-appstorage.md#storagelink)、[\@ObjectLink](arkts-observed-and-objectlink.md)、[\@Consume](arkts-provide-and-consume.md)等自动更新值的状态变量进行更新,可能触发不必要的组件刷新。
734
735```ts
736// MyDataSource类实现IDataSource接口。
737class MyDataSource implements IDataSource {
738  private dataArray: number[] = [];
739
740  public pushData(data: number): void {
741    this.dataArray.push(data);
742  }
743
744  // 数据源的数据总量。
745  public totalCount(): number {
746    return this.dataArray.length;
747  }
748
749  // 返回指定索引位置的数据。
750  public getData(index: number): number {
751    return this.dataArray[index];
752  }
753
754  registerDataChangeListener(listener: DataChangeListener): void {
755  }
756
757  unregisterDataChangeListener(listener: DataChangeListener): void {
758  }
759}
760
761@Entry
762@Component
763struct MyComponent {
764  // 数据源。
765  private data: MyDataSource = new MyDataSource();
766
767  aboutToAppear() {
768    for (let i = 1; i < 1000; i++) {
769      this.data.pushData(i);
770    }
771  }
772
773  build() {
774    Column({ space: 5 }) {
775      Grid() {
776        LazyForEach(this.data, (item: number) => {
777          GridItem() {
778            // 使用可复用自定义组件。
779            ReusableChildComponent({ item: item })
780          }
781        }, (item: string) => item)
782      }
783      .cachedCount(2) // 设置GridItem的缓存数量。
784      .columnsTemplate('1fr 1fr 1fr')
785      .columnsGap(10)
786      .rowsGap(10)
787      .margin(10)
788      .height(500)
789      .backgroundColor(0xFAEEE0)
790    }
791  }
792}
793
794@Reusable
795@Component
796struct ReusableChildComponent {
797  @State item: number = 0;
798
799  // aboutToReuse从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容。
800  // aboutToReuse参数类型已不支持any,这里使用Record指定明确的数据类型。Record用于构造一个对象类型,其属性键为Keys,属性值为Type。
801  aboutToReuse(params: Record<string, number>) {
802    this.item = params.item;
803  }
804
805  build() {
806    Column() {
807      // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错。
808      Image($r('app.media.app_icon'))
809        .objectFit(ImageFit.Fill)
810        .layoutWeight(1)
811      Text(`图片${this.item}`)
812        .fontSize(16)
813        .textAlign(TextAlign.Center)
814    }
815    .width('100%')
816    .height(120)
817    .backgroundColor(0xF9CF93)
818  }
819}
820```
821
822### WaterFlow使用场景
823
824- 在WaterFlow滑动场景中,FlowItem及其子组件频繁创建和销毁。可以将FlowItem中的组件封装成自定义组件,并使用\@Reusable装饰器修饰,实现组件复用。
825
826```ts
827class WaterFlowDataSource implements IDataSource {
828  private dataArray: number[] = [];
829  private listeners: DataChangeListener[] = [];
830
831  constructor() {
832    for (let i = 0; i <= 60; i++) {
833      this.dataArray.push(i);
834    }
835  }
836
837  // 获取索引对应的数据。
838  public getData(index: number): number {
839    return this.dataArray[index];
840  }
841
842  // 通知控制器增加数据。
843  notifyDataAdd(index: number): void {
844    this.listeners.forEach(listener => {
845      listener.onDataAdd(index);
846    });
847  }
848
849  // 获取数据总数。
850  public totalCount(): number {
851    return this.dataArray.length;
852  }
853
854  // 注册改变数据的控制器。
855  registerDataChangeListener(listener: DataChangeListener): void {
856    if (this.listeners.indexOf(listener) < 0) {
857      this.listeners.push(listener);
858    }
859  }
860
861  // 注销改变数据的控制器。
862  unregisterDataChangeListener(listener: DataChangeListener): void {
863    const pos = this.listeners.indexOf(listener);
864    if (pos >= 0) {
865      this.listeners.splice(pos, 1);
866    }
867  }
868
869  // 在数据尾部增加一个元素。
870  public addLastItem(): void {
871    this.dataArray.splice(this.dataArray.length, 0, this.dataArray.length);
872    this.notifyDataAdd(this.dataArray.length - 1);
873  }
874}
875
876@Reusable
877@Component
878struct ReusableFlowItem {
879  @State item: number = 0;
880
881  // 从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容。
882  aboutToReuse(params: ESObject) {
883    this.item = params.item;
884    console.log("=====aboutToReuse====FlowItem==复用了==" + this.item);
885  }
886
887  aboutToRecycle(): void {
888    console.log("=====aboutToRecycle====FlowItem==回收了==" + this.item);
889  }
890
891  build() {
892    // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错。
893    Column() {
894      Text("N" + this.item).fontSize(24).height('26').margin(10)
895      Image($r('app.media.app_icon'))
896        .objectFit(ImageFit.Cover)
897        .width(50)
898        .height(50)
899    }
900  }
901}
902
903@Entry
904@Component
905struct Index {
906  @State minSize: number = 50;
907  @State maxSize: number = 80;
908  @State fontSize: number = 24;
909  @State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F];
910  scroller: Scroller = new Scroller();
911  dataSource: WaterFlowDataSource = new WaterFlowDataSource();
912  private itemWidthArray: number[] = [];
913  private itemHeightArray: number[] = [];
914
915  // 计算flow item宽/高。
916  getSize() {
917    let ret = Math.floor(Math.random() * this.maxSize);
918    return (ret > this.minSize ? ret : this.minSize);
919  }
920
921  // 保存flow item宽/高。
922  getItemSizeArray() {
923    for (let i = 0; i < 100; i++) {
924      this.itemWidthArray.push(this.getSize());
925      this.itemHeightArray.push(this.getSize());
926    }
927  }
928
929  aboutToAppear() {
930    this.getItemSizeArray();
931  }
932
933  build() {
934    Stack({ alignContent: Alignment.TopStart }) {
935      Column({ space: 2 }) {
936        Button('back top')
937          .height('5%')
938          .onClick(() => {
939
940            // 点击后回到顶部。
941            this.scroller.scrollEdge(Edge.Top);
942          })
943        WaterFlow({ scroller: this.scroller }) {
944          LazyForEach(this.dataSource, (item: number) => {
945            FlowItem() {
946              ReusableFlowItem({ item: item })
947            }.onAppear(() => {
948              if (item + 20 == this.dataSource.totalCount()) {
949                for (let i = 0; i < 50; i++) {
950                  this.dataSource.addLastItem();
951                }
952              }
953            })
954
955          })
956        }
957      }
958    }
959  }
960}
961```
962
963### Swiper使用场景
964
965- 在Swiper滑动场景中,条目中的子组件频繁创建和销毁。可以将这些子组件封装成自定义组件,并使用\@Reusable装饰器修饰,以实现组件复用。
966
967```ts
968@Entry
969@Component
970struct Index {
971  private dataSource = new MyDataSource<Question>();
972
973  aboutToAppear(): void {
974    for (let i = 0; i < 1000; i++) {
975      let title = i + 1 + "test_swiper";
976      let answers = ["test1", "test2", "test3",
977        "test4"];
978      // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错。
979      this.dataSource.pushData(new Question(i.toString(), title, $r('app.media.app_icon'), answers));
980    }
981  }
982
983  build() {
984    Column({ space: 5 }) {
985      Swiper() {
986        LazyForEach(this.dataSource, (item: Question) => {
987          QuestionSwiperItem({ itemData: item })
988        }, (item: Question) => item.id)
989      }
990    }
991    .width('100%')
992    .margin({ top: 5 })
993  }
994}
995
996class Question {
997  id: string = '';
998  title: ResourceStr = '';
999  image: ResourceStr = '';
1000  answers: Array<ResourceStr> = [];
1001
1002  constructor(id: string, title: ResourceStr, image: ResourceStr, answers: Array<ResourceStr>) {
1003    this.id = id;
1004    this.title = title;
1005    this.image = image;
1006    this.answers = answers;
1007  }
1008}
1009
1010@Reusable
1011@Component
1012struct QuestionSwiperItem {
1013  @State itemData: Question | null = null;
1014
1015  aboutToReuse(params: Record<string, Object>): void {
1016    this.itemData = params.itemData as Question;
1017    console.info("===aboutToReuse====QuestionSwiperItem==");
1018  }
1019
1020  build() {
1021    Column() {
1022      Text(this.itemData?.title)
1023        .fontSize(18)
1024        .fontColor($r('sys.color.ohos_id_color_primary'))
1025        .alignSelf(ItemAlign.Start)
1026        .margin({
1027          top: 10,
1028          bottom: 16
1029        })
1030      Image(this.itemData?.image)
1031        .width('100%')
1032        .borderRadius(12)
1033        .objectFit(ImageFit.Contain)
1034        .margin({
1035          bottom: 16
1036        })
1037        .height(80)
1038        .width(80)
1039
1040      Column({ space: 16 }) {
1041        ForEach(this.itemData?.answers, (item: Resource) => {
1042          Text(item)
1043            .fontSize(16)
1044            .fontColor($r('sys.color.ohos_id_color_primary'))
1045        }, (item: ResourceStr) => JSON.stringify(item))
1046      }
1047      .width('100%')
1048      .alignItems(HorizontalAlign.Start)
1049    }
1050    .width('100%')
1051    .padding({
1052      left: 16,
1053      right: 16
1054    })
1055  }
1056}
1057
1058class BasicDataSource<T> implements IDataSource {
1059  private listeners: DataChangeListener[] = [];
1060  private originDataArray: T[] = [];
1061
1062  public totalCount(): number {
1063    return 0;
1064  }
1065
1066  public getData(index: number): T {
1067    return this.originDataArray[index];
1068  }
1069
1070  registerDataChangeListener(listener: DataChangeListener): void {
1071    if (this.listeners.indexOf(listener) < 0) {
1072      this.listeners.push(listener);
1073    }
1074  }
1075
1076  unregisterDataChangeListener(listener: DataChangeListener): void {
1077    const pos = this.listeners.indexOf(listener);
1078    if (pos >= 0) {
1079      this.listeners.splice(pos, 1);
1080    }
1081  }
1082
1083  notifyDataAdd(index: number): void {
1084    this.listeners.forEach(listener => {
1085      listener.onDataAdd(index);
1086    });
1087  }
1088}
1089
1090export class MyDataSource<T> extends BasicDataSource<T> {
1091  private dataArray: T[] = [];
1092
1093  public totalCount(): number {
1094    return this.dataArray.length;
1095  }
1096
1097  public getData(index: number): T {
1098    return this.dataArray[index];
1099  }
1100
1101  public pushData(data: T): void {
1102    this.dataArray.push(data);
1103    this.notifyDataAdd(this.dataArray.length - 1);
1104  }
1105}
1106```
1107
1108### 列表滚动-ListItemGroup使用场景
1109
1110- 可以视作特殊List滑动场景,将ListItem需要移除重建的子组件封装成自定义组件,并使用\@Reusable装饰器修饰,使其具备组件复用能力。
1111
1112```ts
1113@Entry
1114@Component
1115struct ListItemGroupAndReusable {
1116  data: DataSrc2 = new DataSrc2();
1117
1118  @Builder
1119  itemHead(text: string) {
1120    Text(text)
1121      .fontSize(20)
1122      .backgroundColor(0xAABBCC)
1123      .width('100%')
1124      .padding(10)
1125  }
1126
1127  aboutToAppear() {
1128    for (let i = 0; i < 10000; i++) {
1129      let data_1 = new DataSrc1();
1130      for (let j = 0; j < 12; j++) {
1131        data_1.Data.push(`测试条目数据: ${i} - ${j}`);
1132      }
1133      this.data.Data.push(data_1);
1134    }
1135  }
1136
1137  build() {
1138    Stack() {
1139      List() {
1140        LazyForEach(this.data, (item: DataSrc1, index: number) => {
1141          ListItemGroup({ header: this.itemHead(index.toString()) }) {
1142            LazyForEach(item, (ii: string, index: number) => {
1143              ListItem() {
1144                Inner({ str: ii })
1145              }
1146            })
1147          }
1148          .width('100%')
1149          .height('60vp')
1150        })
1151      }
1152    }
1153    .width('100%')
1154    .height('100%')
1155  }
1156}
1157
1158@Reusable
1159@Component
1160struct Inner {
1161  @State str: string = '';
1162
1163  aboutToReuse(param: ESObject) {
1164    this.str = param.str;
1165  }
1166
1167  build() {
1168    Text(this.str)
1169  }
1170}
1171
1172class DataSrc1 implements IDataSource {
1173  listeners: DataChangeListener[] = [];
1174  Data: string[] = [];
1175
1176  public totalCount(): number {
1177    return this.Data.length;
1178  }
1179
1180  public getData(index: number): string {
1181    return this.Data[index];
1182  }
1183
1184  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听。
1185  registerDataChangeListener(listener: DataChangeListener): void {
1186    if (this.listeners.indexOf(listener) < 0) {
1187      this.listeners.push(listener);
1188    }
1189  }
1190
1191  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听。
1192  unregisterDataChangeListener(listener: DataChangeListener): void {
1193    const pos = this.listeners.indexOf(listener);
1194    if (pos >= 0) {
1195      this.listeners.splice(pos, 1);
1196    }
1197  }
1198
1199  // 通知LazyForEach组件需要重载所有子组件。
1200  notifyDataReload(): void {
1201    this.listeners.forEach(listener => {
1202      listener.onDataReloaded();
1203    });
1204  }
1205
1206  // 通知LazyForEach组件需要在index对应索引处添加子组件。
1207  notifyDataAdd(index: number): void {
1208    this.listeners.forEach(listener => {
1209      listener.onDataAdd(index);
1210    });
1211  }
1212
1213  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件。
1214  notifyDataChange(index: number): void {
1215    this.listeners.forEach(listener => {
1216      listener.onDataChange(index);
1217    });
1218  }
1219
1220  // 通知LazyForEach组件需要在index对应索引处删除该子组件。
1221  notifyDataDelete(index: number): void {
1222    this.listeners.forEach(listener => {
1223      listener.onDataDelete(index);
1224    });
1225  }
1226
1227  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换。
1228  notifyDataMove(from: number, to: number): void {
1229    this.listeners.forEach(listener => {
1230      listener.onDataMove(from, to);
1231    });
1232  }
1233}
1234
1235class DataSrc2 implements IDataSource {
1236  listeners: DataChangeListener[] = [];
1237  Data: DataSrc1[] = [];
1238
1239  public totalCount(): number {
1240    return this.Data.length;
1241  }
1242
1243  public getData(index: number): DataSrc1 {
1244    return this.Data[index];
1245  }
1246
1247  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听。
1248  registerDataChangeListener(listener: DataChangeListener): void {
1249    if (this.listeners.indexOf(listener) < 0) {
1250      this.listeners.push(listener);
1251    }
1252  }
1253
1254  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听。
1255  unregisterDataChangeListener(listener: DataChangeListener): void {
1256    const pos = this.listeners.indexOf(listener);
1257    if (pos >= 0) {
1258      this.listeners.splice(pos, 1);
1259    }
1260  }
1261
1262  // 通知LazyForEach组件需要重载所有子组件。
1263  notifyDataReload(): void {
1264    this.listeners.forEach(listener => {
1265      listener.onDataReloaded();
1266    });
1267  }
1268
1269  // 通知LazyForEach组件需要在index对应索引处添加子组件。
1270  notifyDataAdd(index: number): void {
1271    this.listeners.forEach(listener => {
1272      listener.onDataAdd(index);
1273    });
1274  }
1275
1276  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件。
1277  notifyDataChange(index: number): void {
1278    this.listeners.forEach(listener => {
1279      listener.onDataChange(index);
1280    });
1281  }
1282
1283  // 通知LazyForEach组件需要在index对应索引处删除该子组件。
1284  notifyDataDelete(index: number): void {
1285    this.listeners.forEach(listener => {
1286      listener.onDataDelete(index);
1287    });
1288  }
1289
1290  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换。
1291  notifyDataMove(from: number, to: number): void {
1292    this.listeners.forEach(listener => {
1293      listener.onDataMove(from, to);
1294    });
1295  }
1296}
1297```
1298
1299
1300### 多种条目类型使用场景
1301
1302**标准型**
1303
1304复用组件的布局相同,示例参见本文列表滚动部分的描述。
1305
1306**有限变化型**
1307
1308复用组件间存在差异,但类型有限。例如,可以通过显式设置两个reuseId或使用两个自定义组件来实现复用。
1309
1310```ts
1311class MyDataSource implements IDataSource {
1312  private dataArray: string[] = [];
1313  private listener: DataChangeListener | undefined;
1314
1315  public totalCount(): number {
1316    return this.dataArray.length;
1317  }
1318
1319  public getData(index: number): string {
1320    return this.dataArray[index];
1321  }
1322
1323  public pushData(data: string): void {
1324    this.dataArray.push(data);
1325  }
1326
1327  public reloadListener(): void {
1328    this.listener?.onDataReloaded();
1329  }
1330
1331  public registerDataChangeListener(listener: DataChangeListener): void {
1332    this.listener = listener;
1333  }
1334
1335  public unregisterDataChangeListener(listener: DataChangeListener): void {
1336    this.listener = undefined;
1337  }
1338}
1339
1340@Entry
1341@Component
1342struct Index {
1343  private data: MyDataSource = new MyDataSource();
1344
1345  aboutToAppear() {
1346    for (let i = 0; i < 1000; i++) {
1347      this.data.pushData(i + "");
1348    }
1349  }
1350
1351  build() {
1352    Column() {
1353      List({ space: 10 }) {
1354        LazyForEach(this.data, (item: number) => {
1355          ListItem() {
1356            ReusableComponent({ item: item })
1357              // 设置两种有限变化的reuseId
1358              .reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo')
1359          }
1360          .backgroundColor(Color.Orange)
1361          .width('100%')
1362        }, (item: number) => item.toString())
1363      }
1364      .cachedCount(2)
1365    }
1366  }
1367}
1368
1369@Reusable
1370@Component
1371struct ReusableComponent {
1372  @State item: number = 0;
1373
1374  aboutToReuse(params: ESObject) {
1375    this.item = params.item;
1376  }
1377
1378  build() {
1379    Column() {
1380      // 组件内部根据类型差异渲染
1381      if (this.item % 2 === 0) {
1382        Text(`Item ${this.item} ReusableComponentOne`)
1383          .fontSize(20)
1384          .margin({ left: 10 })
1385      } else {
1386        Text(`Item ${this.item} ReusableComponentTwo`)
1387          .fontSize(20)
1388          .margin({ left: 10 })
1389      }
1390    }.margin({ left: 10, right: 10 })
1391  }
1392}
1393```
1394
1395**组合型**
1396
1397复用组件间存在多种差异,但通常具备共同的子组件。将三种复用组件以组合型方式转换为Builder函数后,内部的共享子组件将统一置于父组件MyComponent之下。复用这些子组件时,缓存池在父组件层面实现共享,减少组件创建过程中的资源消耗。
1398
1399```ts
1400class MyDataSource implements IDataSource {
1401  private dataArray: string[] = [];
1402  private listener: DataChangeListener | undefined;
1403
1404  public totalCount(): number {
1405    return this.dataArray.length;
1406  }
1407
1408  public getData(index: number): string {
1409    return this.dataArray[index];
1410  }
1411
1412  public pushData(data: string): void {
1413    this.dataArray.push(data);
1414  }
1415
1416  public reloadListener(): void {
1417    this.listener?.onDataReloaded();
1418  }
1419
1420  public registerDataChangeListener(listener: DataChangeListener): void {
1421    this.listener = listener;
1422  }
1423
1424  public unregisterDataChangeListener(listener: DataChangeListener): void {
1425    this.listener = undefined;
1426  }
1427}
1428
1429@Entry
1430@Component
1431struct MyComponent {
1432  private data: MyDataSource = new MyDataSource();
1433
1434  aboutToAppear() {
1435    for (let i = 0; i < 1000; i++) {
1436      this.data.pushData(i.toString());
1437    }
1438  }
1439
1440  // itemBuilderOne作为复用组件的写法未展示,以下为转为Builder之后的写法。
1441  @Builder
1442  itemBuilderOne(item: string) {
1443    Column() {
1444      ChildComponentA({ item: item })
1445      ChildComponentB({ item: item })
1446      ChildComponentC({ item: item })
1447    }
1448  }
1449
1450  // itemBuilderTwo转为Builder之后的写法。
1451  @Builder
1452  itemBuilderTwo(item: string) {
1453    Column() {
1454      ChildComponentA({ item: item })
1455      ChildComponentC({ item: item })
1456      ChildComponentD({ item: item })
1457    }
1458  }
1459
1460  // itemBuilderThree转为Builder之后的写法。
1461  @Builder
1462  itemBuilderThree(item: string) {
1463    Column() {
1464      ChildComponentA({ item: item })
1465      ChildComponentB({ item: item })
1466      ChildComponentD({ item: item })
1467    }
1468  }
1469
1470  build() {
1471    List({ space: 40 }) {
1472      LazyForEach(this.data, (item: string, index: number) => {
1473        ListItem() {
1474          if (index % 3 === 0) {
1475            this.itemBuilderOne(item)
1476          } else if (index % 5 === 0) {
1477            this.itemBuilderTwo(item)
1478          } else {
1479            this.itemBuilderThree(item)
1480          }
1481        }
1482        .backgroundColor('#cccccc')
1483        .width('100%')
1484        .onAppear(() => {
1485          console.log(`ListItem ${index} onAppear`);
1486        })
1487      }, (item: number) => item.toString())
1488    }
1489    .width('100%')
1490    .height('100%')
1491    .cachedCount(0)
1492  }
1493}
1494
1495@Reusable
1496@Component
1497struct ChildComponentA {
1498  @State item: string = '';
1499
1500  aboutToReuse(params: ESObject) {
1501    console.log(`ChildComponentA ${params.item} Reuse ${this.item}`);
1502    this.item = params.item;
1503  }
1504
1505  aboutToRecycle(): void {
1506    console.log(`ChildComponentA ${this.item} Recycle`);
1507  }
1508
1509  build() {
1510    Column() {
1511      Text(`Item ${this.item} Child Component A`)
1512        .fontSize(20)
1513        .margin({ left: 10 })
1514        .fontColor(Color.Blue)
1515      Grid() {
1516        ForEach((new Array(20)).fill(''), (item: string, index: number) => {
1517          GridItem() {
1518            // 请开发者自行在src/main/resources/base/media路径下添加app.media.startIcon图片,否则运行时会因资源缺失而报错。
1519            Image($r('app.media.startIcon'))
1520              .height(20)
1521          }
1522        })
1523      }
1524      .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
1525      .rowsTemplate('1fr 1fr 1fr 1fr')
1526      .columnsGap(10)
1527      .width('90%')
1528      .height(160)
1529    }
1530    .margin({ left: 10, right: 10 })
1531    .backgroundColor(0xFAEEE0)
1532  }
1533}
1534
1535@Reusable
1536@Component
1537struct ChildComponentB {
1538  @State item: string = '';
1539
1540  aboutToReuse(params: ESObject) {
1541    this.item = params.item;
1542  }
1543
1544  build() {
1545    Row() {
1546      Text(`Item ${this.item} Child Component B`)
1547        .fontSize(20)
1548        .margin({ left: 10 })
1549        .fontColor(Color.Red)
1550    }.margin({ left: 10, right: 10 })
1551  }
1552}
1553
1554@Reusable
1555@Component
1556struct ChildComponentC {
1557  @State item: string = '';
1558
1559  aboutToReuse(params: ESObject) {
1560    this.item = params.item;
1561  }
1562
1563  build() {
1564    Row() {
1565      Text(`Item ${this.item} Child Component C`)
1566        .fontSize(20)
1567        .margin({ left: 10 })
1568        .fontColor(Color.Green)
1569    }.margin({ left: 10, right: 10 })
1570  }
1571}
1572
1573@Reusable
1574@Component
1575struct ChildComponentD {
1576  @State item: string = '';
1577
1578  aboutToReuse(params: ESObject) {
1579    this.item = params.item;
1580  }
1581
1582  build() {
1583    Row() {
1584      Text(`Item ${this.item} Child Component D`)
1585        .fontSize(20)
1586        .margin({ left: 10 })
1587        .fontColor(Color.Orange)
1588    }.margin({ left: 10, right: 10 })
1589  }
1590}
1591```
1592