• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# LazyForEach迁移Repeat指导文档
2<!--Kit: ArkUI-->
3<!--Subsystem: ArkUI-->
4<!--Owner: @maorh-->
5<!--Designer: @keerecles-->
6<!--Tester: @TerryTsao-->
7<!--Adviser: @zhang_yixin13-->
8
9[Repeat](./arkts-new-rendering-control-repeat.md)是ArkUI在API version 12中新引入的循环渲染组件,相比[LazyForEach](./arkts-rendering-control-lazyforeach.md)具有更简洁的API、更丰富的功能以及更强的性能优化能力。本指南帮助开发者将LazyForEach平滑地迁移到Repeat。
10
11## 基础用法迁移
12
13### 数据首次渲染
14
15**LazyForEach用例**
16
17LazyForEach根据数据源循环渲染子组件。
18
19示例1中,在容器组件[List](../arkts-layout-development-create-list.md)中使用LazyForEach,并基于数据源循环渲染出了一系列[Text](../arkts-common-components-text-display.md)子组件。
20
21**示例1 - 迁移前**
22
23```ts
24/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
25
26class MyDataSource extends BasicDataSource {
27  private dataArray: string[] = [];
28
29  public totalCount(): number {
30    return this.dataArray.length;
31  }
32
33  public getData(index: number): string {
34    return this.dataArray[index];
35  }
36
37  public pushData(data: string): void {
38    this.dataArray.push(data);
39    this.notifyDataAdd(this.dataArray.length - 1);
40  }
41}
42
43@Entry
44@Component
45struct MyComponent {
46  private data: MyDataSource = new MyDataSource();
47
48  aboutToAppear() {
49    for (let i = 0; i <= 20; i++) {
50      this.data.pushData(`Hello ${i}`);
51    }
52  }
53
54  build() {
55    List({ space: 3 }) {
56      LazyForEach(this.data, (item: string) => {
57        ListItem() {
58          Row() {
59            Text(item).fontSize(50)
60              .onAppear(() => {
61                console.info(`appear: ${item}`);
62              })
63          }.margin({ left: 10, right: 10 })
64        }
65      }, (item: string) => item)
66    }.cachedCount(5)
67  }
68}
69```
70
71以上是一个典型的使用LazyForEach循环渲染子组件的场景,下面将介绍如何将此示例迁移至Repeat。
72
73**迁移步骤**
74
751. 使用状态管理V2装饰器。
76
77   Repeat推荐和状态管理V2装饰器配合使用([懒加载](./arkts-new-rendering-control-repeat.md#循环渲染能力说明)模式下只支持和状态管理V2装饰器配合使用)。如果之前使用的是状态管理V1装饰器,需要修改为状态管理V2装饰器。
78
79   ```ts
80   // 迁移前 - LazyForEach
81   @Component // 状态管理V1
82   struct MyComponent {
83     build() {
84       // ...
85       LazyForEach(...)
86       // ...
87     }
88     // ...其他属性、方法
89   }
90
91   // 迁移后 - Repeat
92   @ComponentV2 // 状态管理V2
93   struct MyComponent {
94     build() {
95       // ...
96       Repeat(...)
97       // ...
98     }
99     // ...其他属性、方法
100   }
101   ```
102
1032. 迁移数据源。
104
105   LazyForEach使用专用的数据结构[IDataSource](../../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md#idatasource)作为数据源。迁移至Repeat后,不再使用IDataSource作为数据源,而是使用状态管理V2装饰的数组作为数据源。
106
107   ```ts
108   // 迁移前 - LazyForEach
109   class MyDataSource implements IDataSource {
110     private dataArray: string[] = [];
111
112     public totalCount(): number {
113       return this.dataArray.length;
114     }
115
116     public getData(index: number): string {
117       return this.dataArray[index];
118     }
119
120     // ...其他方法
121   }
122
123   // 迁移后 - Repeat
124   @Local data: Array<string> = [];
125   ```
126
1273. 迁移组件生成函数和键值生成函数。
128
129   LazyForEach与Repeat均通过组件生成函数,为每一项数据创建一个子组件;通过键值生成函数,为每一项数据生成一个唯一的键值。</br>
130   从LazyForEach迁移至Repeat时,两者的语法存在差异。Repeat需要在[`.each()`](../../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md#each)或[`.template()`](../../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md#template)中设置组件生成函数,在[`.key()`](../../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md#key)中设置键值生成函数。
131
132   ```ts
133   // 迁移前 - LazyForEach
134   List() {
135     LazyForEach(
136       this.data, // 数据源
137       (item: string, index: number) => { // 组件生成函数
138         ListItem() {
139           Text(item)
140         }
141       },
142       (item: string, index: number) => item // 键值生成函数
143     )
144   }
145
146   // 迁移后 - Repeat
147   List() {
148     Repeat<string>(this.data) // 数据源
149       .each((repeatItem: RepeatItem<string>) => { // 组件生成函数
150         ListItem() {
151           Text(repeatItem.item)
152         }
153       })
154       .key((item: string, index: number) => item) // 键值生成函数
155   }
156   ```
157
1584. 配置懒加载功能。
159
160   Repeat具有[懒加载](./arkts-new-rendering-control-repeat.md#循环渲染能力说明)和[全量加载](./arkts-new-rendering-control-repeat.md#关闭懒加载)两种模式。
161
162   - 全量加载模式渲染所有子节点(对标[ForEach](./arkts-rendering-control-foreach.md))。
163   - 懒加载模式动态渲染屏幕区域和预加载区域内的子节点(需要与容器组件配合使用,对标LazyForEach)。
164
165   从LazyForEach迁移至Repeat时,需要调用[virtualScroll](../../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md#virtualscroll))属性,使能懒加载。
166
167   ```ts
168   // 迁移前 - LazyForEach
169   LazyForEach(data, (item) => {...}, (item) => item)
170
171   // 迁移后 - Repeat
172   Repeat(data)
173     .virtualScroll() // 使能懒加载
174   ```
175
176**迁移后代码**
177
178通过以上步骤,可以将示例1从LazyForEach迁移至Repeat,迁移后的完整示例如下所示。
179
180**示例1 - 迁移后**
181
182```ts
183@Entry
184@ComponentV2 // 使用状态管理V2
185struct MyComponent {
186  @Local data: Array<string> = []; // 数据源为状态管理V2装饰的数组
187
188  aboutToAppear() {
189    for (let i = 0; i <= 20; i++) {
190      this.data.push(`Hello ${i}`);
191    }
192  }
193
194  build() {
195    List({ space: 3 }) {
196      Repeat(this.data) // 使用Repeat
197        .each((repeatItem: RepeatItem<string>) => { // 组件生成函数
198          ListItem() {
199            Row() {
200              Text(repeatItem.item).fontSize(50)
201                .onAppear(() => {
202                  console.info(`appear: ${repeatItem.item}`);
203                })
204            }.margin({ left: 10, right: 10 })
205          }
206        })
207        .key((item: string) => item) // 键值生成函数
208        .virtualScroll() // 使能懒加载
209    }.cachedCount(5)
210  }
211}
212```
213
214运行后界面如下图所示。
215
216![LazyForEach-Repeat-Migration-Demo-1](./figures/LazyForEach-Repeat-Migration-Demo-1.gif)
217
218### 数据更新操作
219
220**LazyForEach用例**
221
222当LazyForEach的数据源发生变化时,开发者需要根据数据源的变化情况调用[DataChangeListener](../../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md#datachangelistener)对应的接口,通知LazyForEach做相应的更新。主要的数据操作包括:添加数据、删除数据、交换数据、修改单个数据、修改多个数据、精准批量修改数据。
223
224示例2演示了主要的数据操作。
225
226**示例2 - 迁移前**
227
228```ts
229/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
230
231class MyDataSource extends BasicDataSource {
232  private dataArray: string[] = [];
233
234  public totalCount(): number {
235    return this.dataArray.length;
236  }
237
238  public getData(index: number): string {
239    return this.dataArray[index];
240  }
241
242  // 添加数据
243  public pushData(data: string): void {
244    this.dataArray.push(data);
245    this.notifyDataAdd(this.dataArray.length - 1);
246  }
247
248  // 删除数据
249  public deleteData(index: number): void {
250    this.dataArray.splice(index, 1);
251    this.notifyDataDelete(index);
252  }
253
254  // 交换数据
255  public moveData(from: number, to: number): void {
256    let temp: string = this.dataArray[from];
257    this.dataArray[from] = this.dataArray[to];
258    this.dataArray[to] = temp;
259    this.notifyDataMove(from, to);
260  }
261
262  // 修改单个数据
263  public changeData(index: number, data: string): void {
264    this.dataArray.splice(index, 1, data);
265    this.notifyDataChange(index);
266  }
267
268  // 修改多个数据
269  public modifyAllData(): void {
270    this.dataArray = this.dataArray.map((item: string) => {
271        return 'Changed ' + item;
272    });
273    this.notifyDataReload();
274  }
275}
276
277@Entry
278@Component
279struct MyComponent {
280  private data: MyDataSource = new MyDataSource();
281  private count: number = 0;
282
283  aboutToAppear() {
284    for (let i = 0; i <= 10; i++) {
285      this.data.pushData(`Hello ${i}`);
286    }
287  }
288
289  build() {
290    Column({ space: 3 }) {
291      // 点击追加子组件
292      Button('Add new item')
293        .onClick(() => {
294          this.data.pushData(`New item ${this.count++}`);
295        })
296      // 点击删除子组件
297      Button('Delete item 0')
298        .onClick(() => {
299          this.data.deleteData(0);
300        })
301      // 点击交换子组件
302      Button('Swap item 0 and item 1')
303        .onClick(() => {
304          this.data.moveData(0, 1);
305        })
306      // 点击修改单个子组件
307      Button('Change item 0')
308        .onClick(() => {
309          this.data.changeData(0, `Changed item ${this.count++}`);
310        })
311      // 点击修改多个子组件
312      Button('Change all items')
313        .onClick(() => {
314          this.data.modifyAllData();
315        })
316      List({ space: 3 }) {
317        LazyForEach(this.data, (item: string) => {
318          ListItem() {
319            Row() {
320              Text(item).fontSize(25)
321            }
322          }
323        }, (item: string) => item)
324      }.cachedCount(5)
325    }
326  }
327}
328```
329
330以上是一个典型的更新数据后LazyForEach重新渲染子组件的场景,下面将介绍如何将此示例迁移至Repeat。
331
332**迁移步骤**
333
3341. 迁移准备。
335
336   根据数据首次渲染小节中的步骤,将LazyForEach替换为Repeat。
337     1. 使用状态管理V2装饰器。
338     2. 迁移数据源。
339     3. 迁移组件生成函数与键值生成函数。
340     4. 使能懒加载。
341
3422. 迁移数据源修改方式。
343
344   - 对于LazyForEach,在修改数据源后需要调用对应的接口通知其更新。
345   - 对于Repeat,由状态管理V2监听其数据源变化,并触发更新。因此,开发者直接修改数据源即可,无需其他额外操作。
346
347   ```ts
348   // 以修改单个数据为例
349   // 迁移前 - LazyForEach
350   class MyDataSource implements IDataSource {
351     private dataArray: string[] = [];
352
353     public changeData(index: number, newData: string): void {
354       this.dataArray.splice(index, 1, data);
355       this.notifyDataChange(index);
356     }
357
358     // ...其他方法
359   }
360
361   // 迁移后 - Repeat
362   this.data.splice(index, 1, data);
363   ```
364
365   其他数据更新操作,如添加数据、删除数据、交换数据等,与以上方法类似,可通过直接修改数据源数组实现。
366
367**迁移后代码**
368
369迁移后的完整示例如下。
370
371**示例2 - 迁移后**
372
373```ts
374@Entry
375@ComponentV2
376struct MyComponent {
377  @Local data: Array<string> = [];
378  private count: number = 0;
379
380  aboutToAppear() {
381    for (let i = 0; i <= 10; i++) {
382      this.data.push(`Hello ${i}`);
383    }
384  }
385
386  build() {
387    Column({ space: 3 }) {
388      // 点击追加子组件
389      Button('Add new item')
390        .onClick(() => { this.data.push(`New item ${this.count++}`); })
391      // 点击删除子组件
392      Button('Delete item 0')
393        .onClick(() => { this.data.splice(0, 1); })
394      // 点击交换子组件
395      Button('Swap item 0 and item 1')
396        .onClick(() => { let temp: string = this.data[0];
397                         this.data[0] = this.data[1];
398                         this.data[1] = temp; })
399      // 点击修改单个子组件
400      Button('Change item 0')
401        .onClick(() => { this.data.splice(0, 1, `Changed item ${this.count++}`); })
402      // 点击修改多个子组件
403      Button('Change all items')
404        .onClick(() => { this.data = this.data.map((item: string) => { return 'Changed ' + item; }); })
405      List({ space: 3 }) {
406        Repeat(this.data)
407          .each((repeatItem: RepeatItem<string>) => {
408            ListItem() {
409              Row() {
410                Text(repeatItem.item).fontSize(25)
411              }
412            }
413          })
414          .key((item: string) => item)
415          .virtualScroll()
416      }.cachedCount(5)
417    }
418  }
419}
420```
421
422运行后界面如下图所示。
423
424![LazyForEach-Repeat-Migration-Demo-2](./figures/LazyForEach-Repeat-Migration-Demo-2.gif)
425
426## 典型场景迁移
427
428### 修改数据子属性
429
430**LazyForEach用例**
431
432LazyForEach可以使用[@Observed与@ObjectLink](./arkts-observed-and-objectlink.md)装饰器实现对数据子属性的观测。当有数据子属性发生变化时,仅更新使用了该子属性的组件,从而提高性能。
433
434示例3演示了对子属性的观测。
435
436**示例3 - 迁移前**
437
438```ts
439/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
440
441class MyDataSource extends BasicDataSource {
442  private dataArray: StringData[] = [];
443
444  public totalCount(): number {
445    return this.dataArray.length;
446  }
447
448  public getData(index: number): StringData {
449    return this.dataArray[index];
450  }
451
452  public pushData(data: StringData): void {
453    this.dataArray.push(data);
454    this.notifyDataAdd(this.dataArray.length - 1);
455  }
456}
457
458@Observed
459class StringData {
460  message: string;
461
462  constructor(message: string) {
463    this.message = message;
464  }
465}
466
467@Entry
468@Component
469struct MyComponent {
470  private data: MyDataSource = new MyDataSource();
471
472  aboutToAppear() {
473    for (let i = 0; i <= 20; i++) {
474      this.data.pushData(new StringData(`Hello ${i}`));
475    }
476  }
477
478  build() {
479    List({ space: 3 }) {
480      LazyForEach(this.data, (item: StringData, index: number) => {
481        ListItem() {
482          ChildComponent({ data: item })
483        }
484        .onClick(() => {
485          item.message += '0';
486        })
487      }, (item: StringData, index: number) => index.toString())
488    }.cachedCount(5)
489  }
490}
491
492@Component
493struct ChildComponent {
494  @ObjectLink data: StringData;
495
496  build() {
497    Row() {
498      Text(this.data.message).fontSize(50)
499        .onAppear(() => {
500          console.info(`appear: ${this.data.message}`);
501        })
502    }.margin({ left: 10, right: 10 })
503  }
504}
505```
506
507**迁移Repeat**
508
509Repeat需要和状态管理V2一起使用,状态管理V2提供了[@ObserveV2和@Trace](./arkts-new-observedV2-and-trace.md)装饰器对子属性进行深度观测。迁移时,需要将@Observe和@ObjectLink装饰器迁移至@ObserveV2和@Trace装饰器。
510
511迁移后的示例如下所示。
512
513**示例3 - 迁移后**
514
515```ts
516@ObservedV2
517class StringData {
518  @Trace message: string; // 观测子属性
519
520  constructor(message: string) {
521    this.message = message;
522  }
523}
524
525@Entry
526@ComponentV2
527struct MyComponent {
528  @Local data: StringData[] = [];
529
530  aboutToAppear() {
531    for (let i = 0; i <= 20; i++) {
532      this.data.push(new StringData(`Hello ${i}`));
533    }
534  }
535
536  build() {
537    List({ space: 3 }) {
538      Repeat(this.data)
539        .each((repeatItem) => {
540          ListItem() {
541            Text(repeatItem.item.message).fontSize(50)
542              .onAppear(() => {
543                console.info(`appear: ${repeatItem.item.message}`);
544              })
545          }
546          .onClick(() => {
547            repeatItem.item.message += '0';
548          })
549        })
550        .key((item: StringData, index: number) => index.toString())
551        .virtualScroll()
552    }.cachedCount(5)
553  }
554}
555```
556
557运行后界面如下图所示。
558
559![LazyForEach-Repeat-Migration-Demo-3](./figures/LazyForEach-Repeat-Migration-Demo-3.gif)
560
561### 状态管理V2观测组件内部状态
562
563**LazyForEach用例**
564
565状态管理V2的[@Local](./arkts-new-local.md)装饰器提供了观测自定义组件内部变量的能力。被@Local装饰的变量发生变化时,会通知LazyForEach更新对应的组件。
566
567示例4演示了在LazyForEach中使用@Local装饰器观测数据变化,触发组件更新。
568
569**示例4 - 迁移前**
570
571```ts
572/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
573
574class MyDataSource extends BasicDataSource {
575  private dataArray: StringData[] = [];
576
577  public totalCount(): number {
578    return this.dataArray.length;
579  }
580
581  public getData(index: number): StringData {
582    return this.dataArray[index];
583  }
584
585  public pushData(data: StringData): void {
586    this.dataArray.push(data);
587    this.notifyDataAdd(this.dataArray.length - 1);
588  }
589}
590
591@ObservedV2
592class StringData {
593  @Trace message: string;
594
595  constructor(message: string) {
596    this.message = message;
597  }
598}
599
600@Entry
601@ComponentV2
602struct MyComponent {
603  data: MyDataSource = new MyDataSource();
604
605  aboutToAppear() {
606    for (let i = 0; i <= 20; i++) {
607      this.data.pushData(new StringData(`Hello ${i}`));
608    }
609  }
610
611  build() {
612    List({ space: 3 }) {
613      LazyForEach(this.data, (item: StringData, index: number) => {
614        ListItem() {
615          Row() {
616            Text(item.message).fontSize(50)
617              .onClick(() => {
618                // 修改@ObservedV2装饰类中@Trace装饰的变量,触发刷新此处Text组件
619                item.message += '!';
620              })
621            ChildComponent()
622          }
623        }
624      }, (item: StringData, index: number) => index.toString())
625    }.cachedCount(5)
626  }
627}
628
629@ComponentV2
630struct ChildComponent {
631  @Local message: string = '?';
632
633  build() {
634    Row() {
635      Text(this.message).fontSize(50)
636        .onClick(() => {
637          // 修改@Local装饰的变量,触发刷新此处Text组件
638          this.message += '?';
639        })
640    }
641  }
642}
643```
644
645**迁移Repeat**
646
647Repeat本身支持与状态管理V2联合使用,将LazyForEach相关代码修改为Repeat后,即可实现对组件内部状态变量的观测。
648
649迁移后的示例如下所示。
650
651**示例4 - 迁移后**
652
653```ts
654@ObservedV2
655class StringData {
656  @Trace message: string;
657
658  constructor(message: string) {
659    this.message = message;
660  }
661}
662
663@Entry
664@ComponentV2
665struct MyComponent {
666  @Local data: StringData[] = [];
667
668  aboutToAppear() {
669    for (let i = 0; i <= 20; i++) {
670      this.data.push(new StringData(`Hello ${i}`));
671    }
672  }
673
674  build() {
675    List({ space: 3 }) {
676      Repeat(this.data)
677        .each((repeatItem) => {
678          ListItem() {
679            Row() {
680              Text(repeatItem.item.message).fontSize(50)
681                .onClick(() => {
682                  // 修改@ObservedV2装饰类中@Trace装饰的变量,触发刷新此处Text组件
683                  repeatItem.item.message += '!';
684                })
685              ChildComponent()
686            }
687          }
688        })
689        .key((item: StringData, index: number) => index.toString())
690        .virtualScroll()
691    }.cachedCount(5)
692  }
693}
694
695@ComponentV2
696struct ChildComponent {
697  @Local message: string = '?';
698
699  build() {
700    Row() {
701      Text(this.message).fontSize(50)
702        .onClick(() => {
703          // 修改@Local装饰的变量,触发刷新此处Text组件
704          this.message += '?';
705        })
706    }
707  }
708}
709```
710
711运行后界面如下图所示。
712
713![LazyForEach-Repeat-Migration-Demo-4](./figures/LazyForEach-Repeat-Migration-Demo-4.gif)
714
715### 状态管理V2观测组件外部输入
716
717**LazyForEach用例**
718
719状态管理V2的[@Param](./arkts-new-param.md)装饰器提供了观测自定义组件外部输入变量的能力,可以实现父子组件间的数据同步。将父组件的变量传递给子组件,并用@Param装饰,当父组件变量发生变化时,会通知对应的组件更新。
720
721示例5演示了在LazyForEach中使用@Param装饰器观测数据变化,触发组件更新。
722
723
724**示例5 - 迁移前**
725
726```ts
727/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
728
729class MyDataSource extends BasicDataSource {
730  private dataArray: StringData[] = [];
731
732  public totalCount(): number {
733    return this.dataArray.length;
734  }
735
736  public getData(index: number): StringData {
737    return this.dataArray[index];
738  }
739
740  public pushData(data: StringData): void {
741    this.dataArray.push(data);
742    this.notifyDataAdd(this.dataArray.length - 1);
743  }
744}
745
746@ObservedV2
747class StringData {
748  @Trace message: string;
749
750  constructor(message: string) {
751    this.message = message;
752  }
753}
754
755@Entry
756@ComponentV2
757struct MyComponent {
758  data: MyDataSource = new MyDataSource();
759
760  aboutToAppear() {
761    for (let i = 0; i <= 20; i++) {
762      this.data.pushData(new StringData(`Hello ${i}`));
763    }
764  }
765
766  build() {
767    List({ space: 3 }) {
768      LazyForEach(this.data, (item: StringData, index: number) => {
769        ListItem() {
770          ChildComponent({ data: item.message }) // 向自定义组件内传入变量
771            .onClick(() => {
772              item.message += '!';
773            })
774        }
775      }, (item: StringData, index: number) => index.toString())
776    }.cachedCount(5)
777  }
778}
779
780@ComponentV2
781struct ChildComponent {
782  @Param @Require data: string = ''; // 接收来自外部的变量
783
784  build() {
785    Row() {
786      Text(this.data).fontSize(50)
787    }
788  }
789}
790```
791
792**迁移Repeat**
793
794Repeat本身支持与状态管理V2联合使用,将LazyForEach相关代码修改为Repeat后,即可实现对组件外部输入状态变量的观测。
795
796迁移后的示例如下所示。
797
798**示例5 - 迁移后**
799
800```ts
801@ObservedV2
802class StringData {
803  @Trace message: string;
804
805  constructor(message: string) {
806    this.message = message;
807  }
808}
809
810@Entry
811@ComponentV2
812struct MyComponent {
813  @Local data: StringData[] = [];
814
815  aboutToAppear() {
816    for (let i = 0; i <= 20; i++) {
817      this.data.push(new StringData(`Hello ${i}`));
818    }
819  }
820
821  build() {
822    List({ space: 3 }) {
823      Repeat(this.data)
824        .each((repeatItem) => {
825          ListItem() {
826            ChildComponent({ data: repeatItem.item.message }) // 向自定义组件内传入变量
827              .onClick(() => {
828                repeatItem.item.message += '!';
829              })
830          }
831        })
832        .key((item: StringData, index: number) => index.toString())
833        .virtualScroll()
834    }.cachedCount(5)
835  }
836}
837
838@ComponentV2
839struct ChildComponent {
840  @Param @Require data: string = ''; // 接收来自外部的变量
841
842  build() {
843    Row() {
844      Text(this.data).fontSize(50)
845    }
846  }
847}
848```
849
850运行后界面如下图所示。
851
852![LazyForEach-Repeat-Migration-Demo-5](./figures/LazyForEach-Repeat-Migration-Demo-5.gif)
853
854### 拖拽排序
855
856**LazyForEach用例**
857
858LazyForEach的[onMove](../../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-sorting.md#onmove)属性提供了拖拽排序能力。
859
860示例6为典型用例。
861
862**示例6 - 迁移前**
863
864```ts
865/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
866
867class MyDataSource extends BasicDataSource {
868  private dataArray: string[] = [];
869
870  public totalCount(): number {
871    return this.dataArray.length;
872  }
873
874  public getData(index: number): string {
875    return this.dataArray[index];
876  }
877
878  public moveDataWithoutNotify(from: number, to: number): void {
879    let tmp = this.dataArray.splice(from, 1);
880    this.dataArray.splice(to, 0, tmp[0]);
881  }
882
883  public pushData(data: string): void {
884    this.dataArray.push(data);
885    this.notifyDataAdd(this.dataArray.length - 1);
886  }
887}
888
889@Entry
890@Component
891struct Parent {
892  private data: MyDataSource = new MyDataSource();
893
894  aboutToAppear(): void {
895    for (let i = 0; i < 100; i++) {
896      this.data.pushData(i.toString());
897    }
898  }
899
900  build() {
901    Row() {
902      List() {
903        LazyForEach(this.data, (item: string) => {
904          ListItem() {
905            Text(item.toString())
906              .fontSize(16)
907              .textAlign(TextAlign.Center)
908              .size({ height: 100, width: '100%' })
909          }.margin(10)
910          .borderRadius(10)
911          .backgroundColor('#FFFFFFFF')
912        }, (item: string) => item)
913          .onMove((from: number, to: number) => { // 实现拖拽排序
914            this.data.moveDataWithoutNotify(from, to);
915          })
916      }
917      .width('100%')
918      .height('100%')
919      .backgroundColor('#FFDCDCDC')
920    }
921  }
922}
923```
924
925**迁移Repeat**
926
927Repeat具有与LazyForEach相同的onMove属性。将LazyForEach相关代码修改为Repeat后,即可实现拖拽排序。
928
929迁移后的示例如下所示。
930
931**示例6 - 迁移后**
932
933```ts
934@Entry
935@ComponentV2
936struct Parent {
937  @Local data: string[] = [];
938
939  aboutToAppear(): void {
940    for (let i = 0; i < 100; i++) {
941      this.data.push(i.toString());
942    }
943  }
944
945  moveData(from: number, to: number) {
946    let tmp = this.data.splice(from, 1);
947    this.data.splice(to, 0, tmp[0]);
948  }
949
950  build() {
951    Row() {
952      List() {
953        Repeat(this.data)
954          .each((repeatItem) => {
955            ListItem() {
956              Text(repeatItem.item.toString())
957                .fontSize(16)
958                .textAlign(TextAlign.Center)
959                .size({ height: 100, width: '100%' })
960            }.margin(10)
961            .borderRadius(10)
962            .backgroundColor('#FFFFFFFF')
963          })
964          .key((item: string) => item)
965          .virtualScroll()
966          .onMove((from: number, to: number) => { // 实现拖拽排序
967            this.moveData(from, to);
968          })
969      }
970      .width('100%')
971      .height('100%')
972      .backgroundColor('#FFDCDCDC')
973    }
974  }
975}
976```
977
978运行后界面如下图所示。
979
980![LazyForEach-Repeat-Migration-Demo-6](./figures/LazyForEach-Repeat-Migration-Demo-6.gif)
981
982### 组件复用
983
984**LazyForEach用例**
985
986LazyForEach自身并不具备组件复用能力,为实现组件复用,需要与[@Reusable](./arkts-reusable.md)装饰器配合使用(被@Reusable装饰的自定义组件具有复用能力)。
987
988示例7演示了组件复用的典型场景。
989
990**示例7 - 迁移前**
991
992```ts
993/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
994
995class MyDataSource extends BasicDataSource {
996  private dataArray: StringData[] = [];
997
998  public totalCount(): number {
999    return this.dataArray.length;
1000  }
1001
1002  public getData(index: number): StringData {
1003    return this.dataArray[index];
1004  }
1005
1006  public pushData(data: StringData): void {
1007    this.dataArray.push(data);
1008    this.notifyDataAdd(this.dataArray.length - 1);
1009  }
1010}
1011
1012class StringData {
1013  message: string;
1014
1015  constructor(message: string) {
1016    this.message = message;
1017  }
1018}
1019
1020@Entry
1021@Component
1022struct MyComponent {
1023  data: MyDataSource = new MyDataSource();
1024
1025  aboutToAppear() {
1026    for (let i = 0; i <= 30; i++) {
1027      this.data.pushData(new StringData(`Hello${i}`));
1028    }
1029  }
1030
1031  build() {
1032    List({ space: 3 }) {
1033      LazyForEach(this.data, (item: StringData, index: number) => {
1034        ListItem() {
1035          ChildComponent({ data: item })
1036            .onAppear(() => {
1037              console.info(`onAppear: ${item.message}`);
1038            })
1039        }
1040      }, (item: StringData, index: number) => index.toString())
1041    }.cachedCount(5)
1042  }
1043}
1044
1045@Reusable
1046@Component
1047struct ChildComponent {
1048  @State data: StringData = new StringData('');
1049
1050  aboutToAppear(): void {
1051    console.info(`aboutToAppear: ${this.data.message}`);
1052  }
1053
1054  aboutToRecycle(): void {
1055    console.info(`aboutToRecycle: ${this.data.message}`);
1056  }
1057
1058  // 对复用的组件进行数据更新
1059  aboutToReuse(params: Record<string, ESObject>): void {
1060    this.data = params.data as StringData;
1061    console.info(`aboutToReuse: ${this.data.message}`);
1062  }
1063
1064  build() {
1065    Row() {
1066      Text(this.data.message).fontSize(50)
1067    }
1068  }
1069}
1070```
1071
1072**迁移Repeat**
1073
1074Repeat本身具备组件复用能力,同时也支持与状态管理V2的[@ReusableV2](./arkts-new-reusableV2.md)装饰器联合使用。因此,迁移至Repeat后,其组件复用具有两种实现方案。
1075
10761. 直接使用Repeat自身的复用能力。
10772. 使用@ReusableV2装饰器提供的复用能力。
1078
1079需要注意的是,Repeat默认使能自身的复用能力,且优先级高于@ReusableV2装饰器。若要使用@ReusableV2装饰器,需要先手动关闭Repeat自身的复用能力(@ReusableV2装饰器从API version 18开始支持,Repeat从API version 19开始支持关闭自身复用能力)。
1080
1081**示例7 - 迁移方案1:使用Repeat自身的复用能力**
1082
1083Repeat本身具备复用能力,且默认开启。将LazyForEach相关代码迁移至Repeat后,便已经具备了复用能力。
1084
1085修改后的示例如下。
1086
1087```ts
1088class StringData {
1089  message: string;
1090
1091  constructor(message: string) {
1092    this.message = message;
1093  }
1094}
1095
1096@Entry
1097@ComponentV2
1098struct MyComponent {
1099  @Local data: StringData[] = [];
1100
1101  aboutToAppear() {
1102    for (let i = 0; i <= 30; i++) {
1103      this.data.push(new StringData(`Hello${i}`));
1104    }
1105  }
1106
1107  build() {
1108    List({ space: 3 }) {
1109      Repeat(this.data) // Repeat自身具备复用功能
1110        .each((repeatItem) => {
1111          ListItem() {
1112            Text(repeatItem.item.message).fontSize(50)
1113          }
1114        })
1115        .key((item: StringData, index: number) => index.toString())
1116        .virtualScroll()
1117    }.cachedCount(5)
1118  }
1119}
1120```
1121
1122**示例7 - 迁移方案2:使用@ReusableV2装饰器**
1123
1124若要使用@ReusableV2装饰器,首先需要通过`.virtualScroll({ reusable: false })`关闭Repeat自身的复用功能,再用@ReusableV2装饰需要复用的自定义组件。
1125
1126相较于Repeat自身的复用,@ReusableV2装饰的自定义组件在回收和复用时,会触发aboutToRecycle和aboutToReuse两个生命周期。
1127
1128使用@ReusableV2装饰器的迁移示例如下所示。
1129
1130```ts
1131class StringData {
1132  message: string;
1133
1134  constructor(message: string) {
1135    this.message = message;
1136  }
1137}
1138
1139@Entry
1140@ComponentV2
1141struct MyComponent {
1142  @Local data: StringData[] = [];
1143
1144  aboutToAppear() {
1145    for (let i = 0; i <= 30; i++) {
1146      this.data.push(new StringData(`Hello${i}`));
1147    }
1148  }
1149
1150  build() {
1151    List({ space: 3 }) {
1152      Repeat(this.data)
1153        .each((repeatItem) => {
1154          ListItem() {
1155            ChildComponent({ data: repeatItem.item })
1156              .onAppear(() => {
1157                console.info(`onAppear: ${repeatItem.item.message}`);
1158              })
1159          }
1160        })
1161        .key((item: StringData, index: number) => index.toString())
1162        .virtualScroll({ reusable: false }) // 关闭Repeat自身的复用功能(API 19)
1163    }.cachedCount(5)
1164  }
1165}
1166
1167// 使用@ReusableV2实现组件复用(API 18)
1168@ReusableV2
1169@ComponentV2
1170struct ChildComponent {
1171  @Param data: StringData = new StringData('');
1172
1173  aboutToAppear(): void {
1174    console.info(`aboutToAppear: ${this.data.message}`);
1175  }
1176
1177  aboutToRecycle(): void {
1178    console.info(`aboutToRecycle: ${this.data.message}`);
1179  }
1180
1181  aboutToReuse(): void {
1182    console.info(`aboutToReuse: ${this.data.message}`);
1183  }
1184
1185  build() {
1186    Row() {
1187      Text(this.data.message).fontSize(50)
1188    }
1189  }
1190}
1191```
1192
1193运行后界面如下图所示。
1194
1195![LazyForEach-Repeat-Migration-Demo-7](./figures/LazyForEach-Repeat-Migration-Demo-7.gif)
1196
1197### 模板渲染
1198
1199**LazyForEach用例**
1200
1201LazyForEach自身并不具备模板渲染能力。为实现模板渲染能力,需要开发者自己实现逻辑判断,为不同的数据项选择不同的渲染模板。
1202
1203示例8演示了模板渲染的典型场景。
1204
1205**示例8 - 迁移前**
1206
1207```ts
1208/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
1209
1210class MyDataSource extends BasicDataSource {
1211  private dataArray: StringData[] = [];
1212
1213  public totalCount(): number {
1214    return this.dataArray.length;
1215  }
1216
1217  public getData(index: number): StringData {
1218    return this.dataArray[index];
1219  }
1220
1221  public pushData(data: StringData): void {
1222    this.dataArray.push(data);
1223    this.notifyDataAdd(this.dataArray.length - 1);
1224  }
1225}
1226
1227class StringData {
1228  message: string;
1229  type: number;
1230
1231  constructor(message: string, type: number) {
1232    this.message = message;
1233    this.type = type;
1234  }
1235
1236  getType(): number {
1237    if (this.type >= 1) {
1238      return 1;
1239    } else {
1240      return 0;
1241    }
1242  }
1243}
1244
1245@Entry
1246@Component
1247struct MyComponent {
1248  data: MyDataSource = new MyDataSource();
1249
1250  aboutToAppear() {
1251    for (let i = 0; i <= 200; i++) {
1252      this.data.pushData(new StringData(`Hello${i}`, i % 2));
1253    }
1254  }
1255
1256  build() {
1257    List({ space: 3 }) {
1258      LazyForEach(this.data, (item: StringData, index: number) => {
1259        ListItem() {
1260          // 开发者自己实现逻辑判断,为不同的数据项选择不同的渲染模板
1261          if (item.getType() == 0) {
1262            // 模板A
1263            ChildComponentA({ data: item })
1264              .onAppear(() => {
1265                console.info(`type A onAppear: ${item.message}`);
1266              })
1267          } else {
1268            // 模板B
1269            ChildComponentB({ data: item })
1270              .onAppear(() => {
1271                console.info(`type B onAppear: ${item.message}`);
1272              })
1273          }
1274        }
1275      }, (item: StringData, index: number) => index.toString())
1276    }.cachedCount(5)
1277  }
1278}
1279
1280// 使用@Reusable实现组件复用
1281@Reusable
1282@Component
1283struct ChildComponentA {
1284  @State data: StringData = new StringData('', 0);
1285
1286  aboutToAppear(): void {
1287    console.info(`type A aboutToAppear: ${this.data.message}`);
1288  }
1289
1290  aboutToRecycle(): void {
1291    console.info(`type A aboutToRecycle: ${this.data.message}`);
1292  }
1293
1294  aboutToReuse(params: Record<string, ESObject>): void {
1295    this.data = params.data as StringData;
1296    console.info(`type A aboutToReuse: ${this.data.message}`);
1297  }
1298
1299  build() {
1300    Row() {
1301      Text(this.data.message).fontSize(50)
1302      Button('Type A')
1303    }
1304  }
1305}
1306
1307@Reusable
1308@Component
1309struct ChildComponentB {
1310  @State data: StringData = new StringData('', 0);
1311
1312  aboutToAppear(): void {
1313    console.info(`type B aboutToAppear: ${this.data.message}`);
1314  }
1315
1316  aboutToRecycle(): void {
1317    console.info(`type B aboutToRecycle: ${this.data.message}`);
1318  }
1319
1320  aboutToReuse(params: Record<string, ESObject>): void {
1321    this.data = params.data as StringData;
1322    console.info(`type B aboutToReuse: ${this.data.message}`);
1323  }
1324
1325  build() {
1326    Row() {
1327      Text(this.data.message).fontSize(50).fontColor(Color.Gray)
1328      Text('Type B')
1329    }
1330  }
1331}
1332```
1333**迁移Repeat**
1334
1335Repeat本身具备模板渲染能力,开发者可以通过[templateId](../../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md#templateid)方法为不同的数据项选择不同的模板,再通过[template](../../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md#template)方法为不同的模板配置不同的组件生成函数。同时,开发者仍然可以选择自己实现逻辑判断,为不同的数据项分配不同的模板。
1336
1337需要注意的是,如果开发者选择自己实现模板渲染,则需要关闭Repeat自身的复用功能。否则,Repeat在复用子组件时无法选择正确的模板,会导致渲染异常。
1338
1339**示例8 - 迁移方案1:使用Repeat自身的模板渲染能力**
1340
1341```ts
1342class StringData {
1343  message: string;
1344  type: number;
1345
1346  constructor(message: string, type: number) {
1347    this.message = message;
1348    this.type = type;
1349  }
1350
1351  getType(): number {
1352    if (this.type >= 1) {
1353      return 1;
1354    } else {
1355      return 0;
1356    }
1357  }
1358}
1359
1360@Entry
1361@ComponentV2
1362struct MyComponent {
1363  data: StringData[] = [];
1364
1365  aboutToAppear() {
1366    for (let i = 0; i <= 200; i++) {
1367      this.data.push(new StringData(`Hello${i}`, i % 2));
1368    }
1369  }
1370
1371  build() {
1372    List({ space: 3 }) {
1373      Repeat(this.data)
1374        .each((repeatItem) => {
1375          ListItem() {
1376            Text('Default item')
1377          }
1378        })
1379        .template('A', (repeatItem) => { // 模板A
1380          ListItem() {
1381            Row() {
1382              Text(repeatItem.item.message).fontSize(50)
1383              Button('Type A')
1384            }
1385          }
1386        })
1387        .template('B', (repeatItem) => { // 模板B
1388          ListItem() {
1389            Row() {
1390              Text(repeatItem.item.message).fontSize(50).fontColor(Color.Gray)
1391              Text('Type B')
1392            }
1393          }
1394        })
1395        .templateId((item: StringData) => { // 为不同的数据项选择不同的模板
1396          if (item.getType() == 0) {
1397            return 'A';
1398          } else {
1399            return 'B';
1400          }
1401        })
1402        .key((item: StringData, index: number) => index.toString())
1403        .virtualScroll()
1404    }.cachedCount(5)
1405  }
1406}
1407```
1408
1409**示例8 - 迁移方案2:由开发者实现模板渲染能力**
1410
1411```ts
1412class StringData {
1413  message: string;
1414  type: number;
1415
1416  constructor(message: string, type: number) {
1417    this.message = message;
1418    this.type = type;
1419  }
1420
1421  getType(): number {
1422    if (this.type >= 1) {
1423      return 1;
1424    } else {
1425      return 0;
1426    }
1427  }
1428}
1429
1430@Entry
1431@ComponentV2
1432struct MyComponent {
1433  data: StringData[] = [];
1434
1435  aboutToAppear() {
1436    for (let i = 0; i <= 200; i++) {
1437      this.data.push(new StringData(`Hello${i}`, i % 2));
1438    }
1439  }
1440
1441  build() {
1442    List({ space: 3 }) {
1443      Repeat(this.data)
1444        .each((repeatItem) => {
1445          ListItem() {
1446            // 开发者自己实现逻辑判断,为不同的数据项选择不同的渲染模板
1447            if (repeatItem.item.getType() == 0) {
1448              ChildComponentA({ data: repeatItem.item }) // 模板A
1449                .onAppear(() => {
1450                  console.info(`type A onAppear: ${repeatItem.item.message}`);
1451                })
1452            } else {
1453              ChildComponentB({ data: repeatItem.item }) // 模板B
1454                .onAppear(() => {
1455                  console.info(`type B onAppear: ${repeatItem.item.message}`);
1456                })
1457            }
1458          }
1459        })
1460        .key((item: StringData, index: number) => index.toString())
1461        .virtualScroll({ reusable: false }) // 关闭Repeat自身的复用功能(API 19),避免渲染异常
1462    }.cachedCount(5)
1463  }
1464}
1465
1466// 使用@ReusableV2实现组件复用(API 18)
1467@ReusableV2
1468@ComponentV2
1469struct ChildComponentA {
1470  @Param data: StringData = new StringData('', 0);
1471
1472  aboutToAppear(): void {
1473    console.info(`type A aboutToAppear: ${this.data.message}`);
1474  }
1475
1476  aboutToRecycle(): void {
1477    console.info(`type A aboutToRecycle: ${this.data.message}`);
1478  }
1479
1480  aboutToReuse(): void {
1481    console.info(`type A aboutToReuse: ${this.data.message}`);
1482  }
1483
1484  build() {
1485    Row() {
1486      Text(this.data.message).fontSize(50)
1487      Button('Type A')
1488    }
1489  }
1490}
1491
1492@ReusableV2
1493@ComponentV2
1494struct ChildComponentB {
1495  @Param data: StringData = new StringData('', 0);
1496
1497  aboutToAppear(): void {
1498    console.info(`type B aboutToAppear: ${this.data.message}`);
1499  }
1500
1501  aboutToRecycle(): void {
1502    console.info(`type B aboutToRecycle: ${this.data.message}`);
1503  }
1504
1505  aboutToReuse(): void {
1506    console.info(`type B aboutToReuse: ${this.data.message}`);
1507  }
1508
1509  build() {
1510    Row() {
1511      Text(this.data.message).fontSize(50).fontColor(Color.Gray)
1512      Text('Type B')
1513    }
1514  }
1515}
1516```
1517
1518运行后界面如下图所示。
1519
1520![LazyForEach-Repeat-Migration-Demo-8](./figures/LazyForEach-Repeat-Migration-Demo-8.gif)
1521
1522## BasicDataSource示例代码
1523
1524### string类型数组的BasicDataSource代码
1525
1526```ts
1527// BasicDataSource实现了IDataSource接口,用于管理listener监听,以及通知LazyForEach数据更新
1528class BasicDataSource implements IDataSource {
1529  private listeners: DataChangeListener[] = [];
1530  private originDataArray: string[] = [];
1531
1532  public totalCount(): number {
1533    return this.originDataArray.length;
1534  }
1535
1536  public getData(index: number): string {
1537    return this.originDataArray[index];
1538  }
1539
1540  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
1541  registerDataChangeListener(listener: DataChangeListener): void {
1542    if (this.listeners.indexOf(listener) < 0) {
1543      console.info('add listener');
1544      this.listeners.push(listener);
1545    }
1546  }
1547
1548  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
1549  unregisterDataChangeListener(listener: DataChangeListener): void {
1550    const pos = this.listeners.indexOf(listener);
1551    if (pos >= 0) {
1552      console.info('remove listener');
1553      this.listeners.splice(pos, 1);
1554    }
1555  }
1556
1557  // 通知LazyForEach组件需要重载所有子组件
1558  notifyDataReload(): void {
1559    this.listeners.forEach(listener => {
1560      listener.onDataReloaded();
1561    });
1562  }
1563
1564  // 通知LazyForEach组件需要在index对应索引处添加子组件
1565  notifyDataAdd(index: number): void {
1566    this.listeners.forEach(listener => {
1567      listener.onDataAdd(index);
1568      // 写法2:listener.onDatasetChange([{type: DataOperationType.ADD, index: index}]);
1569    });
1570  }
1571
1572  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
1573  notifyDataChange(index: number): void {
1574    this.listeners.forEach(listener => {
1575      listener.onDataChange(index);
1576      // 写法2:listener.onDatasetChange([{type: DataOperationType.CHANGE, index: index}]);
1577    });
1578  }
1579
1580  // 通知LazyForEach组件需要在index对应索引处删除该子组件
1581  notifyDataDelete(index: number): void {
1582    this.listeners.forEach(listener => {
1583      listener.onDataDelete(index);
1584      // 写法2:listener.onDatasetChange([{type: DataOperationType.DELETE, index: index}]);
1585    });
1586  }
1587
1588  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换
1589  notifyDataMove(from: number, to: number): void {
1590    this.listeners.forEach(listener => {
1591      listener.onDataMove(from, to);
1592      // 写法2:listener.onDatasetChange(
1593      //         [{type: DataOperationType.EXCHANGE, index: {start: from, end: to}}]);
1594    });
1595  }
1596
1597  notifyDatasetChange(operations: DataOperation[]): void {
1598    this.listeners.forEach(listener => {
1599      listener.onDatasetChange(operations);
1600    });
1601  }
1602}
1603```
1604
1605### StringData类型数组的BasicDataSource代码
1606
1607```ts
1608class BasicDataSource implements IDataSource {
1609  private listeners: DataChangeListener[] = [];
1610  private originDataArray: StringData[] = [];
1611
1612  public totalCount(): number {
1613    return this.originDataArray.length;
1614  }
1615
1616  public getData(index: number): StringData {
1617    return this.originDataArray[index];
1618  }
1619
1620  registerDataChangeListener(listener: DataChangeListener): void {
1621    if (this.listeners.indexOf(listener) < 0) {
1622      console.info('add listener');
1623      this.listeners.push(listener);
1624    }
1625  }
1626
1627  unregisterDataChangeListener(listener: DataChangeListener): void {
1628    const pos = this.listeners.indexOf(listener);
1629    if (pos >= 0) {
1630      console.info('remove listener');
1631      this.listeners.splice(pos, 1);
1632    }
1633  }
1634
1635  notifyDataReload(): void {
1636    this.listeners.forEach(listener => {
1637      listener.onDataReloaded();
1638    });
1639  }
1640
1641  notifyDataAdd(index: number): void {
1642    this.listeners.forEach(listener => {
1643      listener.onDataAdd(index);
1644    });
1645  }
1646
1647  notifyDataChange(index: number): void {
1648    this.listeners.forEach(listener => {
1649      listener.onDataChange(index);
1650    });
1651  }
1652
1653  notifyDataDelete(index: number): void {
1654    this.listeners.forEach(listener => {
1655      listener.onDataDelete(index);
1656    });
1657  }
1658
1659  notifyDataMove(from: number, to: number): void {
1660    this.listeners.forEach(listener => {
1661      listener.onDataMove(from, to);
1662    });
1663  }
1664
1665  notifyDatasetChange(operations: DataOperation[]): void {
1666    this.listeners.forEach(listener => {
1667      listener.onDatasetChange(operations);
1668    });
1669  }
1670}
1671```