• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# LazyForEach:数据懒加载
2<!--Kit: ArkUI-->
3<!--Subsystem: ArkUI-->
4<!--Owner: @maorh-->
5<!--Designer: @keerecles-->
6<!--Tester: @TerryTsao-->
7<!--Adviser: @zhang_yixin13-->
8
9API参数说明见:[LazyForEach API参数说明](../../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md)。
10
11## 概述
12
13LazyForEach为开发者提供了基于数据源渲染出一系列子组件的能力。具体而言,LazyForEach从数据源中按需迭代数据,并在每次迭代时创建相应组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会销毁并回收组件以降低内存占用。</br>
14本文档依次介绍了LazyForEach的[基本用法](#基本用法)、[高级用法](#高级用法)和[常见问题](#常见问题),开发者可以按需阅读。在[首次渲染](#首次渲染)小节中,给出了简单的示例,可以帮助开发者快速上手LazyForEach的使用。
15
16> **说明:**
17>
18> 在大量子组件的的场景下,LazyForEach与缓存列表项、动态预加载、组件复用等方法配合使用,可以进一步提升滑动帧率并降低应用内存占用。最佳实践请参考[优化长列表加载慢丢帧问题](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-best-practices-long-list)19
20## 使用限制
21
22- LazyForEach必须在容器组件内使用,仅有[List](../../reference/apis-arkui/arkui-ts/ts-container-list.md)、[ListItemGroup](../../reference/apis-arkui/arkui-ts/ts-container-listitemgroup.md)、[Grid](../../reference/apis-arkui/arkui-ts/ts-container-grid.md)、[Swiper](../../reference/apis-arkui/arkui-ts/ts-container-swiper.md)以及[WaterFlow](../../reference/apis-arkui/arkui-ts/ts-container-waterflow.md)组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。支持数据懒加载的父组件根据自身及子组件的高度或宽度计算可视区域内需布局的子节点数量,高度或宽度的缺失会导致部分场景[懒加载失效](#懒加载失效)。
23- LazyForEach依赖生成的键值判断是否刷新子组件,键值不变则不触发刷新。
24- 容器组件内只能包含一个LazyForEach。以List为例,不推荐同时包含ListItem、ForEach、LazyForEach。也不推荐同时包含多个LazyForEach。
25- LazyForEach在每次迭代中,必须创建且只允许创建一个子组件;即LazyForEach的子组件生成函数有且只有一个根组件。
26- 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
27- 允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
28- 键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件渲染出现问题。
29- LazyForEach必须使用DataChangeListener对象进行更新,重新赋值第一个参数dataSource会导致异常;dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。
30- 为了高性能渲染,使用DataChangeListener对象的onDataChange方法更新UI时,需要生成不同于原来的键值来触发组件刷新。
31- LazyForEach和[\@Reusable](./arkts-reusable.md)装饰器一起使用能触发节点复用。使用方法:将@Reusable装饰在LazyForEach列表的组件上,见[列表滚动配合LazyForEach使用](./arkts-reusable.md#列表滚动配合lazyforeach使用)。
32- LazyForEach和[\@ReusableV2](./arkts-new-reusableV2.md)装饰器一起使用能触发节点复用。详见[在LazyForEach组件中使用\@ReusableV2](./arkts-new-reusableV2.md#在lazyforeach组件中使用)。
33
34## 基本用法
35
36### 设置数据源
37
38为了管理[DataChangeListener](../../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md#datachangelistener)监听器和通知LazyForEach更新数据,开发者需要使用如下方法:首先实现LazyForEach提供的[IDataSource](../../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md#idatasource)接口,将其作为LazyForEach的数据源,然后管理监听器和更新数据。
39
40为实现基本的数据管理和监听能力,开发者需要实现`IDataSource`的[totalCount](../../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md#totalcount)、[getData](../../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md#getdata)、[registerDataChangeListener](../../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md#registerdatachangelistener)和[unregisterDataChangeListener](../../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md#unregisterdatachangelistener)方法,具体请参考[BasicDataSource示例代码](#basicdatasource示例代码)。当数据源变化时,通过调用监听器的接口通知LazyForEach更新,具体请参考[数据更新](#数据更新)。
41
42### 键值生成规则
43
44在`LazyForEach`循环渲染过程中,系统为每个item生成一个唯一且持久的键值,用于标识对应的组件。键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并基于新的键值创建新的组件。
45
46`LazyForEach`提供了参数`keyGenerator`,开发者可以使用该函数生成自定义键值。如果未定义`keyGenerator`函数,ArkUI框架将使用默认的键值生成函数:`(item: Object, index: number) => { return viewId + '-' + index.toString(); }`。viewId在编译器转换过程中生成,同一个LazyForEach组件内的viewId一致。
47
48### 组件创建规则
49
50在确定键值生成规则后,LazyForEach的第二个参数`itemGenerator`函数会根据组件创建规则为数据源的每个数组项创建组件。组件的创建包括两种情况:LazyForEach[首次渲染](#首次渲染)和LazyForEach数据更新后的[非首次渲染](#数据更新)。
51
52### 首次渲染
53
54使用LazyForEach时,开发者需要提供数据源、键值生成函数和组件创建函数。**开发者需保证键值生成函数为每项数据生成不同的键值。**</br>
55在LazyForEach首次渲染时,会根据上述键值生成规则为数据源的每个数组项生成唯一键值并创建相应的组件。
56
57```ts
58/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
59
60class MyDataSource extends BasicDataSource {
61  private dataArray: string[] = [];
62
63  public totalCount(): number {
64    return this.dataArray.length;
65  }
66
67  public getData(index: number): string {
68    return this.dataArray[index];
69  }
70
71  public pushData(data: string): void {
72    this.dataArray.push(data);
73    this.notifyDataAdd(this.dataArray.length - 1);
74  }
75}
76
77@Entry
78@Component
79struct MyComponent {
80  private data: MyDataSource = new MyDataSource();
81
82  aboutToAppear() {
83    for (let i = 0; i <= 20; i++) {
84      this.data.pushData(`Hello ${i}`);
85    }
86  }
87
88  build() {
89    List({ space: 3 }) {
90      LazyForEach(this.data, (item: string) => {
91        ListItem() {
92          Row() {
93            Text(item).fontSize(50)
94              .onAppear(() => {
95                console.info(`appear: ${item}`);
96              })
97          }.margin({ left: 10, right: 10 })
98        }
99      }, (item: string) => item)
100    }.cachedCount(5)
101  }
102}
103```
104
105在上述代码中,`keyGenerator`函数的返回值是`item`。`LazyForEach`循环渲染时,为数据源数组项依次生成键值`Hello 0`、`Hello 1` ... `Hello 20`,并创建对应的`ListItem`子组件渲染到界面上。
106
107运行效果如下图所示。
108
109**图1**  LazyForEach正常首次渲染
110![LazyForEach-Render-DifferentKey](./figures/LazyForEach-Render-DifferentKey.gif)
111
112**错误案例:键值相同导致渲染异常**
113
114当不同数据项生成的键值相同时,框架的行为是不可预测的。例如,在以下代码中,`LazyForEach`渲染的数据项键值均相同,在滑动过程中,`LazyForEach`会预加载划入划出当前页面的子组件,而新建的子组件和销毁的旧子组件具有相同的键值,框架可能取用错误的缓存,导致子组件渲染出现问题。
115```ts
116/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
117
118class MyDataSource extends BasicDataSource {
119  private dataArray: string[] = [];
120
121  public totalCount(): number {
122    return this.dataArray.length;
123  }
124
125  public getData(index: number): string {
126    return this.dataArray[index];
127  }
128
129  public pushData(data: string): void {
130    this.dataArray.push(data);
131    this.notifyDataAdd(this.dataArray.length - 1);
132  }
133}
134
135@Entry
136@Component
137struct MyComponent {
138  private data: MyDataSource = new MyDataSource();
139
140  aboutToAppear() {
141    for (let i = 0; i <= 20; i++) {
142      this.data.pushData(`Hello ${i}`);
143    }
144  }
145
146  build() {
147    List({ space: 3 }) {
148      LazyForEach(this.data, (item: string) => {
149        ListItem() {
150          Row() {
151            Text(item).fontSize(50)
152              .onAppear(() => {
153                console.info(`appear: ${item}`);
154              })
155          }.margin({ left: 10, right: 10 })
156        }
157      }, (item: string) => `same key`) // 自定义键值生成函数,返回相同键值
158    }.cachedCount(5)
159  }
160}
161```
162
163运行效果如下图所示。
164
165**图2**  LazyForEach存在相同键值
166![LazyForEach-Render-SameKey](./figures/LazyForEach-Render-SameKey.gif)
167
168修改上述示例中LazyForEach的键值生成函数,使每个数据项生成唯一的键值,保证渲染效果符合预期。
169
170```ts
171LazyForEach(this.data, (item: string) => {
172  ListItem() {
173   Row() {
174    Text(item).fontSize(50)
175      .onAppear(() => {
176        console.info(`appear: ${item}`);
177      })
178    }.margin({ left: 10, right: 10 })
179  }
180}, (item: string, index: number) => `${item}-${index}`) // 自定义键值生成函数,返回唯一键值
181```
182
183修改后运行效果如下图所示。
184
185**图3**  LazyForEach生成唯一键值
186![LazyForEach-Render-UniqueKey](./figures/LazyForEach-Render-UniqueKey.gif)
187
188### 数据更新
189
190当`LazyForEach`数据源发生变化,需要再次渲染时,开发者应根据数据源的变化情况调用`listener`对应的接口,通知`LazyForEach`做相应的更新。`LazyForEach`的更新操作包括:添加数据、删除数据、交换数据、改变单个数据、改变多个数据以及精准批量修改数据,各使用场景示例如下。
191
192**添加数据**
193
194```ts
195/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
196
197class MyDataSource extends BasicDataSource {
198  private dataArray: string[] = [];
199
200  public totalCount(): number {
201    return this.dataArray.length;
202  }
203
204  public getData(index: number): string {
205    return this.dataArray[index];
206  }
207
208  public pushData(data: string): void {
209    this.dataArray.push(data);
210    this.notifyDataAdd(this.dataArray.length - 1);
211  }
212}
213
214@Entry
215@Component
216struct MyComponent {
217  private data: MyDataSource = new MyDataSource();
218
219  aboutToAppear() {
220    for (let i = 0; i <= 20; i++) {
221      this.data.pushData(`Hello ${i}`);
222    }
223  }
224
225  build() {
226    List({ space: 3 }) {
227      LazyForEach(this.data, (item: string) => {
228        ListItem() {
229          Row() {
230            Text(item).fontSize(50)
231              .onAppear(() => {
232                console.info(`appear: ${item}`);
233              })
234          }.margin({ left: 10, right: 10 })
235        }
236        .onClick(() => {
237          // 点击追加子组件
238          this.data.pushData(`Hello ${this.data.totalCount()}`);
239        })
240      }, (item: string) => item)
241    }.cachedCount(5)
242  }
243}
244```
245
246点击`LazyForEach`的子组件时,首先调用数据源`data`的`pushData`方法。此方法会在数据源末尾添加数据,并调用`notifyDataAdd`方法。`notifyDataAdd`方法内部会调用`listener.onDataAdd`方法,通知`LazyForEach`有数据添加。`LazyForEach`接收到通知后,在该索引处新建子组件。
247
248运行效果如下图所示。
249
250**图4**  LazyForEach添加数据
251![LazyForEach-Add-Data](./figures/LazyForEach-Add-Data.gif)
252
253**删除数据**
254
255```ts
256/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
257
258class MyDataSource extends BasicDataSource {
259  private dataArray: string[] = [];
260
261  public totalCount(): number {
262    return this.dataArray.length;
263  }
264
265  public getData(index: number): string {
266    return this.dataArray[index];
267  }
268
269  public getAllData(): string[] {
270    return this.dataArray;
271  }
272
273  public pushData(data: string): void {
274    this.dataArray.push(data);
275  }
276
277  public deleteData(index: number): void {
278    this.dataArray.splice(index, 1);
279    this.notifyDataDelete(index);
280  }
281}
282
283@Entry
284@Component
285struct MyComponent {
286  private data: MyDataSource = new MyDataSource();
287
288  aboutToAppear() {
289    for (let i = 0; i <= 20; i++) {
290      this.data.pushData(`Hello ${i}`);
291    }
292  }
293
294  build() {
295    List({ space: 3 }) {
296      LazyForEach(this.data, (item: string, index: number) => {
297        ListItem() {
298          Row() {
299            Text(item).fontSize(50)
300              .onAppear(() => {
301                console.info(`appear: ${item}`);
302              })
303          }.margin({ left: 10, right: 10 })
304        }
305        .onClick(() => {
306          // 点击删除子组件
307          this.data.deleteData(this.data.getAllData().indexOf(item));
308        })
309      }, (item: string) => item)
310    }.cachedCount(5)
311  }
312}
313```
314
315点击`LazyForEach`的子组件时,调用数据源`data`的`deleteData`方法。此方法删除数据源中对应索引的数据,并调用`notifyDataDelete`方法。`notifyDataDelete`方法内调用`listener.onDataDelete`方法,通知 `LazyForEach`删除该索引处的子组件。
316
317运行效果如下图所示。
318
319**图5**  LazyForEach删除数据
320![LazyForEach-Delete-Data](./figures/LazyForEach-Delete-Data.gif)
321
322**交换数据**
323
324```ts
325/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
326
327class MyDataSource extends BasicDataSource {
328  private dataArray: string[] = [];
329
330  public totalCount(): number {
331    return this.dataArray.length;
332  }
333
334  public getData(index: number): string {
335    return this.dataArray[index];
336  }
337
338  public getAllData(): string[] {
339    return this.dataArray;
340  }
341
342  public pushData(data: string): void {
343    this.dataArray.push(data);
344  }
345
346  public moveData(from: number, to: number): void {
347    let temp: string = this.dataArray[from];
348    this.dataArray[from] = this.dataArray[to];
349    this.dataArray[to] = temp;
350    this.notifyDataMove(from, to);
351  }
352}
353
354@Entry
355@Component
356struct MyComponent {
357  private moved: number[] = [];
358  private data: MyDataSource = new MyDataSource();
359
360  aboutToAppear() {
361    for (let i = 0; i <= 20; i++) {
362      this.data.pushData(`Hello ${i}`);
363    }
364  }
365
366  build() {
367    List({ space: 3 }) {
368      LazyForEach(this.data, (item: string, index: number) => {
369        ListItem() {
370          Row() {
371            Text(item).fontSize(50)
372              .onAppear(() => {
373                console.info(`appear: ${item}`);
374              })
375          }.margin({ left: 10, right: 10 })
376        }
377        .onClick(() => {
378          this.moved.push(this.data.getAllData().indexOf(item));
379          if (this.moved.length === 2) {
380            // 点击交换子组件
381            this.data.moveData(this.moved[0], this.moved[1]);
382            this.moved = [];
383          }
384        })
385      }, (item: string) => item)
386    }.cachedCount(5)
387  }
388}
389```
390
391首次点击`LazyForEach`的子组件时,将要移动的数据索引存储在`moved`成员变量中。再次点击`LazyForEach`的另一个子组件时,将首次点击的子组件移到此处。调用数据源`data`的`moveData`方法,该方法将数据源中的数据移动到预期位置,并调用`notifyDataMove`方法。`notifyDataMove`方法会调用`listener.onDataMove`方法,通知`LazyForEach`在该处有数据需要移动。`LazyForEach`将`from`和`to`索引处的子组件进行位置调换。
392
393运行效果如下图所示。
394
395**图6**  LazyForEach交换数据
396![LazyForEach-Exchange-Data](./figures/LazyForEach-Exchange-Data.gif)
397
398**改变单个数据**
399
400```ts
401/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
402
403class MyDataSource extends BasicDataSource {
404  private dataArray: string[] = [];
405
406  public totalCount(): number {
407    return this.dataArray.length;
408  }
409
410  public getData(index: number): string {
411    return this.dataArray[index];
412  }
413
414  public pushData(data: string): void {
415    this.dataArray.push(data);
416  }
417
418  public changeData(index: number, data: string): void {
419    this.dataArray.splice(index, 1, data);
420    this.notifyDataChange(index);
421  }
422}
423
424@Entry
425@Component
426struct MyComponent {
427  private data: MyDataSource = new MyDataSource();
428
429  aboutToAppear() {
430    for (let i = 0; i <= 20; i++) {
431      this.data.pushData(`Hello ${i}`);
432    }
433  }
434
435  build() {
436    List({ space: 3 }) {
437      LazyForEach(this.data, (item: string, index: number) => {
438        ListItem() {
439          Row() {
440            Text(item).fontSize(50)
441              .onAppear(() => {
442                console.info(`appear: ${item}`);
443              })
444          }.margin({ left: 10, right: 10 })
445        }
446        .onClick(() => {
447          this.data.changeData(index, item + '00');
448        })
449      }, (item: string) => item)
450    }.cachedCount(5)
451  }
452}
453```
454
455点击`LazyForEach`的子组件时,首先改变当前数据,然后调用数据源`data`的`changeData`方法。`changeData` 方法会调用`notifyDataChange`方法,该方法又会调用`listener.onDataChange`方法,通知`LazyForEach`组件数据发生变化。`LazyForEach`会在对应索引处重建子组件。
456
457运行效果如下图所示。
458
459**图7**  LazyForEach改变单个数据
460![LazyForEach-Change-SingleData](./figures/LazyForEach-Change-SingleData.gif)
461
462**改变多个数据**
463
464```ts
465/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
466
467class MyDataSource extends BasicDataSource {
468  private dataArray: string[] = [];
469
470  public totalCount(): number {
471    return this.dataArray.length;
472  }
473
474  public getData(index: number): string {
475    return this.dataArray[index];
476  }
477
478  public pushData(data: string): void {
479    this.dataArray.push(data);
480  }
481
482  public reloadData(): void {
483    this.notifyDataReload();
484  }
485
486  public modifyAllData(): void {
487    this.dataArray = this.dataArray.map((item: string) => {
488      return item + '0';
489    });
490  }
491}
492
493@Entry
494@Component
495struct MyComponent {
496  private data: MyDataSource = new MyDataSource();
497
498  aboutToAppear() {
499    for (let i = 0; i <= 20; i++) {
500      this.data.pushData(`Hello ${i}`);
501    }
502  }
503
504  build() {
505    List({ space: 3 }) {
506      LazyForEach(this.data, (item: string, index: number) => {
507        ListItem() {
508          Row() {
509            Text(item).fontSize(50)
510              .onAppear(() => {
511                console.info(`appear: ${item}`);
512              })
513          }.margin({ left: 10, right: 10 })
514        }
515        .onClick(() => {
516          this.data.modifyAllData();
517          this.data.reloadData();
518        })
519      }, (item: string) => item)
520    }.cachedCount(5)
521  }
522}
523```
524
525点击`LazyForEach`的子组件时,首先调用`data`的`modifyAllData`方法修改数据源中的所有数据,然后调用数据源的`reloadData`方法。该方法内会调用`notifyDataReload`方法,`notifyDataReload`方法内会调用`listener.onDataReloaded`方法,通知`LazyForEach`重建所有子节点。`LazyForEach`会将原数据项和新数据项进行键值比对,若键值相同则使用缓存,若键值不同则重新构建。
526
527运行效果如下图所示。
528
529**图8**  LazyForEach改变多个数据
530![LazyForEach-Reload-Data](./figures/LazyForEach-Reload-Data.gif)
531
532**精准批量修改数据**
533
534```ts
535/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
536
537class MyDataSource extends BasicDataSource {
538  private dataArray: string[] = [];
539
540  public totalCount(): number {
541    return this.dataArray.length;
542  }
543
544  public getData(index: number): string {
545    return this.dataArray[index];
546  }
547
548  public operateData(): void {
549    console.info(`[${this.dataArray.join(', ')}]`);
550    this.dataArray.splice(4, 0, this.dataArray[1]);
551    this.dataArray.splice(1, 1);
552    let temp = this.dataArray[4];
553    this.dataArray[4] = this.dataArray[6];
554    this.dataArray[6] = temp;
555    this.dataArray.splice(8, 0, 'Hello 1', 'Hello 2');
556    this.dataArray.splice(12, 2);
557    console.info(`[${this.dataArray.join(', ')}]`);
558    this.notifyDatasetChange([
559      { type: DataOperationType.MOVE, index: { from: 1, to: 3 } },
560      { type: DataOperationType.EXCHANGE, index: { start: 4, end: 6 } },
561      { type: DataOperationType.ADD, index: 8, count: 2 },
562      { type: DataOperationType.DELETE, index: 10, count: 2 }]);
563  }
564
565  public init(): void {
566    this.dataArray.splice(0, 0, 'Hello a', 'Hello b', 'Hello c', 'Hello d', 'Hello e', 'Hello f', 'Hello g', 'Hello h',
567      'Hello i', 'Hello j', 'Hello k', 'Hello l', 'Hello m', 'Hello n', 'Hello o', 'Hello p', 'Hello q', 'Hello r');
568  }
569}
570
571@Entry
572@Component
573struct MyComponent {
574  private data: MyDataSource = new MyDataSource();
575
576  aboutToAppear() {
577    this.data.init();
578  }
579
580  build() {
581    Column() {
582      Text('change data')
583        .fontSize(10)
584        .backgroundColor(Color.Blue)
585        .fontColor(Color.White)
586        .borderRadius(50)
587        .padding(5)
588        .onClick(() => {
589          this.data.operateData();
590        })
591      List({ space: 3 }) {
592        LazyForEach(this.data, (item: string, index: number) => {
593          ListItem() {
594            Row() {
595              Text(item).fontSize(35)
596                .onAppear(() => {
597                  console.info(`appear: ${item}`);
598                })
599            }.margin({ left: 10, right: 10 })
600          }
601
602        }, (item: string) => item + new Date().getTime())
603      }.cachedCount(5)
604    }
605  }
606}
607```
608
609onDatasetChange接口允许开发者一次性通知LazyForEach进行数据添加、删除、移动和交换等操作。在上述例子中,点击“change data”文本后,第二项数据被移动到第四项位置,第五项与第七项数据交换位置,并且从第九项开始添加了数据"Hello 1"和"Hello 2",同时从第十一项开始删除了两项数据。
610
611**图9**  LazyForEach改变多个数据
612![LazyForEach-Change-MultiData](./figures/LazyForEach-Change-MultiData.gif)
613
614第二个例子,直接给数组赋值,不涉及 splice 操作。operations直接从比较原数组和新数组得到。
615
616```ts
617/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
618
619class MyDataSource extends BasicDataSource {
620  private dataArray: string[] = [];
621
622  public totalCount(): number {
623    return this.dataArray.length;
624  }
625
626  public getData(index: number): string {
627    return this.dataArray[index];
628  }
629
630  public operateData(): void {
631    this.dataArray =
632      ['Hello x', 'Hello 1', 'Hello 2', 'Hello b', 'Hello c', 'Hello e', 'Hello d', 'Hello f', 'Hello g', 'Hello h'];
633    this.notifyDatasetChange([
634      { type: DataOperationType.CHANGE, index: 0 },
635      { type: DataOperationType.ADD, index: 1, count: 2 },
636      { type: DataOperationType.EXCHANGE, index: { start: 3, end: 4 } },
637    ]);
638  }
639
640  public init(): void {
641    this.dataArray = ['Hello a', 'Hello b', 'Hello c', 'Hello d', 'Hello e', 'Hello f', 'Hello g', 'Hello h'];
642  }
643}
644
645@Entry
646@Component
647struct MyComponent {
648  private data: MyDataSource = new MyDataSource();
649
650  aboutToAppear() {
651    this.data.init();
652  }
653
654  build() {
655    Column() {
656      Text('Multi-Data Change')
657        .fontSize(10)
658        .backgroundColor(Color.Blue)
659        .fontColor(Color.White)
660        .borderRadius(50)
661        .padding(5)
662        .onClick(() => {
663          this.data.operateData();
664        })
665      List({ space: 3 }) {
666        LazyForEach(this.data, (item: string, index: number) => {
667          ListItem() {
668            Row() {
669              Text(item).fontSize(35)
670                .onAppear(() => {
671                  console.info(`appear: ${item}`);
672                })
673            }.margin({ left: 10, right: 10 })
674          }
675
676        }, (item: string) => item + new Date().getTime())
677      }.cachedCount(5)
678    }
679  }
680}
681```
682
683**图10**  LazyForEach改变多个数据
684![LazyForEach-Change-MultiData2](./figures/LazyForEach-Change-MultiData2.gif)
685
686使用该接口时请注意以下事项。
687
6881. 不要将`onDatasetChange`与其他操作数据的接口混用。
6892. 传入`onDatasetChange`的`operations`中,每一项`operation`的`index`均从修改前的原数组中查找。因此,`operations`中的`index`不总是与`Datasource`中的`index`一一对应,并且不能为负数。
690
691第一个例子清楚地显示了这一点:
692
693```ts
694// 修改之前的数组
695['Hello a','Hello b','Hello c','Hello d','Hello e','Hello f','Hello g','Hello h','Hello i','Hello j','Hello k','Hello l','Hello m','Hello n','Hello o','Hello p','Hello q','Hello r']
696// 修改之后的数组
697['Hello a','Hello c','Hello d','Hello b','Hello g','Hello f','Hello e','Hello h','Hello 1','Hello 2','Hello i','Hello j','Hello m','Hello n','Hello o','Hello p','Hello q','Hello r']
698```
699"Hello b" 从第2项变成第4项,因此第一个 operation 为 `{ type: DataOperationType.MOVE, index: { from: 1, to: 3 } }`。
700"Hello e" 跟 "Hello g" 对调了,而 "Hello e" 在修改前的原数组中的 index=4,"Hello g" 在修改前的原数组中的 index=6, 因此第二个 operation 为 `{ type: DataOperationType.EXCHANGE, index: { start: 4, end: 6 } }`。
701"Hello 1","Hello 2" 在 "Hello h" 之后插入,而 "Hello h" 在修改前的原数组中的 index=7,因此第三个 operation 为 `{ type: DataOperationType.ADD, index: 8, count: 2 }`。
702"Hello k","Hello l" 被删除了,而 "Hello k" 在原数组中的 index=10,因此第四个 operation 为 `{ type: DataOperationType.DELETE, index: 10, count: 2 }`。
703
7043. 在同一个`onDatasetChange`批量处理数据时,如果多个`DataOperation`操作同一个`index`,只有第一个`DataOperation`生效。
7054. 部分操作由开发者传入键值,LazyForEach不再重复调用`keygenerator`获取键值,开发者需保证传入键值的正确性。
7065. 若操作集合中包含RELOAD操作,则其他操作均不生效。
707
708## 高级用法
709
710### 改变数据子属性
711
712若仅靠`LazyForEach`的刷新机制,当`item`变化时若想更新子组件,需要将原来的子组件全部销毁再重新构建,在子组件结构较为复杂的情况下,靠改变键值去刷新渲染性能较低。因此框架提供了[\@Observed和\@ObjectLink](./arkts-observed-and-objectlink.md)机制进行深度观测,可以做到仅刷新使用了该属性的组件,提高渲染性能。开发者可根据其自身业务特点选择使用哪种刷新方式。
713
714```ts
715/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
716
717class MyDataSource extends BasicDataSource {
718  private dataArray: StringData[] = [];
719
720  public totalCount(): number {
721    return this.dataArray.length;
722  }
723
724  public getData(index: number): StringData {
725    return this.dataArray[index];
726  }
727
728  public pushData(data: StringData): void {
729    this.dataArray.push(data);
730    this.notifyDataAdd(this.dataArray.length - 1);
731  }
732}
733
734@Observed
735class StringData {
736  message: string;
737
738  constructor(message: string) {
739    this.message = message;
740  }
741}
742
743@Entry
744@Component
745struct MyComponent {
746  private data: MyDataSource = new MyDataSource();
747
748  aboutToAppear() {
749    for (let i = 0; i <= 20; i++) {
750      this.data.pushData(new StringData(`Hello ${i}`));
751    }
752  }
753
754  build() {
755    List({ space: 3 }) {
756      LazyForEach(this.data, (item: StringData, index: number) => {
757        ListItem() {
758          ChildComponent({ data: item })
759        }
760        .onClick(() => {
761          item.message += '0';
762        })
763      }, (item: StringData, index: number) => index.toString())
764    }.cachedCount(5)
765  }
766}
767
768@Component
769struct ChildComponent {
770  @ObjectLink data: StringData;
771
772  build() {
773    Row() {
774      Text(this.data.message).fontSize(50)
775        .onAppear(() => {
776          console.info(`appear: ${this.data.message}`);
777        })
778    }.margin({ left: 10, right: 10 })
779  }
780}
781```
782
783点击`LazyForEach`子组件改变`item.message`时,重渲染依赖`ChildComponent`的`@ObjectLink`成员变量对子属性的监听。框架仅刷新`Text(this.data.message)`,不会重建整个`ListItem`子组件。
784
785**图11**  LazyForEach改变数据子属性
786![LazyForEach-Change-SubProperty](./figures/LazyForEach-Change-SubProperty.gif)
787
788### 使用状态管理V2
789
790状态管理V2提供[\@ObservedV2和\@Trace](./arkts-new-observedV2-and-trace.md)装饰器,用于实现属性的深度观测。使用[\@Local](./arkts-new-local.md)和[\@Param](./arkts-new-param.md)装饰器,可以管理子组件的刷新,仅刷新使用了对应属性的组件。
791
792**嵌套类属性变化观测**
793
794```ts
795/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
796
797class MyDataSource extends BasicDataSource {
798  private dataArray: StringData[] = [];
799
800  public totalCount(): number {
801    return this.dataArray.length;
802  }
803
804  public getData(index: number): StringData {
805    return this.dataArray[index];
806  }
807
808  public pushData(data: StringData): void {
809    this.dataArray.push(data);
810    this.notifyDataAdd(this.dataArray.length - 1);
811  }
812}
813
814class StringData {
815  firstLayer: FirstLayer;
816
817  constructor(firstLayer: FirstLayer) {
818    this.firstLayer = firstLayer;
819  }
820}
821
822class FirstLayer {
823  secondLayer: SecondLayer;
824
825  constructor(secondLayer: SecondLayer) {
826    this.secondLayer = secondLayer;
827  }
828}
829
830class SecondLayer {
831  thirdLayer: ThirdLayer;
832
833  constructor(thirdLayer: ThirdLayer) {
834    this.thirdLayer = thirdLayer;
835  }
836}
837
838@ObservedV2
839class ThirdLayer {
840  @Trace fourthLayer: string;
841
842  constructor(fourthLayer: string) {
843    this.fourthLayer = fourthLayer;
844  }
845}
846
847@Entry
848@ComponentV2
849struct MyComponent {
850  private data: MyDataSource = new MyDataSource();
851
852  aboutToAppear() {
853    for (let i = 0; i <= 20; i++) {
854      this.data.pushData(new StringData(new FirstLayer(new SecondLayer(new ThirdLayer(`Hello ${i}`)))));
855    }
856  }
857
858  build() {
859    List({ space: 3 }) {
860      LazyForEach(this.data, (item: StringData, index: number) => {
861        ListItem() {
862          Text(item.firstLayer.secondLayer.thirdLayer.fourthLayer).fontSize(50)
863            .onClick(() => {
864              item.firstLayer.secondLayer.thirdLayer.fourthLayer += '!';
865            })
866        }
867      }, (item: StringData, index: number) => index.toString())
868    }.cachedCount(5)
869  }
870}
871```
872
873`@ObservedV2`与`@Trace`用于装饰类以及类中的属性,配合使用能深度观测被装饰的类和属性。示例中,展示了深度嵌套类结构下,通过`@ObservedV2`和`@Trace`实现对多层嵌套属性变化的观测和子组件刷新。当点击子组件`Text`修改被`@Trace`修饰的嵌套类最内层的类成员属性时,仅重新渲染依赖了该属性的组件。
874
875**组件内部状态**
876
877```ts
878/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
879
880class MyDataSource extends BasicDataSource {
881  private dataArray: StringData[] = [];
882
883  public totalCount(): number {
884    return this.dataArray.length;
885  }
886
887  public getData(index: number): StringData {
888    return this.dataArray[index];
889  }
890
891  public pushData(data: StringData): void {
892    this.dataArray.push(data);
893    this.notifyDataAdd(this.dataArray.length - 1);
894  }
895}
896
897@ObservedV2
898class StringData {
899  @Trace message: string;
900
901  constructor(message: string) {
902    this.message = message;
903  }
904}
905
906@Entry
907@ComponentV2
908struct MyComponent {
909  data: MyDataSource = new MyDataSource();
910
911  aboutToAppear() {
912    for (let i = 0; i <= 20; i++) {
913      this.data.pushData(new StringData(`Hello ${i}`));
914    }
915  }
916
917  build() {
918    List({ space: 3 }) {
919      LazyForEach(this.data, (item: StringData, index: number) => {
920        ListItem() {
921          Row() {
922
923            Text(item.message).fontSize(50)
924              .onClick(() => {
925                // 修改@ObservedV2装饰类中@Trace装饰的变量,触发刷新此处Text组件
926                item.message += '!';
927              })
928            ChildComponent()
929          }
930        }
931      }, (item: StringData, index: number) => index.toString())
932    }.cachedCount(5)
933  }
934}
935
936@ComponentV2
937struct ChildComponent {
938  @Local message: string = '?';
939
940  build() {
941    Row() {
942      Text(this.message).fontSize(50)
943        .onClick(() => {
944          // 修改@Local装饰的变量,触发刷新此处Text组件
945          this.message += '?';
946        })
947    }
948  }
949}
950```
951
952`@Local`使得自定义组件内被修饰的变量具有观测其变化的能力,该变量必须在组件内部进行初始化。示例中,点击`Text`组件修改`item.message`触发变量更新并刷新使用该变量的组件,`ChildComponent`中`@Local`装饰的变量`message`变化时也能刷新子组件。
953
954**组件外部输入**
955
956```ts
957/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
958
959class MyDataSource extends BasicDataSource {
960  private dataArray: StringData[] = [];
961
962  public totalCount(): number {
963    return this.dataArray.length;
964  }
965
966  public getData(index: number): StringData {
967    return this.dataArray[index];
968  }
969
970  public pushData(data: StringData): void {
971    this.dataArray.push(data);
972    this.notifyDataAdd(this.dataArray.length - 1);
973  }
974}
975
976@ObservedV2
977class StringData {
978  @Trace message: string;
979
980  constructor(message: string) {
981    this.message = message;
982  }
983}
984
985@Entry
986@ComponentV2
987struct MyComponent {
988  data: MyDataSource = new MyDataSource();
989
990  aboutToAppear() {
991    for (let i = 0; i <= 20; i++) {
992      this.data.pushData(new StringData(`Hello ${i}`));
993    }
994  }
995
996  build() {
997    List({ space: 3 }) {
998      LazyForEach(this.data, (item: StringData, index: number) => {
999        ListItem() {
1000          ChildComponent({ data: item.message })
1001            .onClick(() => {
1002              item.message += '!';
1003            })
1004        }
1005      }, (item: StringData, index: number) => index.toString())
1006    }.cachedCount(5)
1007  }
1008}
1009
1010@ComponentV2
1011struct ChildComponent {
1012  @Param @Require data: string = '';
1013
1014  build() {
1015    Row() {
1016      Text(this.data).fontSize(50)
1017    }
1018  }
1019}
1020```
1021
1022使用`@Param`装饰器,子组件可以接受外部输入参数,实现父子组件间的数据同步。在`MyComponent`中创建子组件时,传递`item.message`,并用`@Param`修饰的变量`data`与其关联。点击`ListItem`中的组件修改`item.message`,数据变化会从父组件传递到子组件,触发子组件刷新。
1023
1024### 拖拽排序
1025当LazyForEach在List组件下使用,并且设置了[onMove](../../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-sorting.md#onmove)事件,可以使能拖拽排序。拖拽排序释放后,如果数据位置发生变化,将触发onMove事件,上报原始索引号和目标索引号。在onMove事件中,根据上报的索引号修改数据源。修改数据源时,无需调用DataChangeListener接口通知数据源变化。
1026
1027```ts
1028/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
1029
1030class MyDataSource extends BasicDataSource {
1031  private dataArray: string[] = [];
1032
1033  public totalCount(): number {
1034    return this.dataArray.length;
1035  }
1036
1037  public getData(index: number): string {
1038    return this.dataArray[index];
1039  }
1040
1041  public moveDataWithoutNotify(from: number, to: number): void {
1042    let tmp = this.dataArray.splice(from, 1);
1043    this.dataArray.splice(to, 0, tmp[0]);
1044  }
1045
1046  public pushData(data: string): void {
1047    this.dataArray.push(data);
1048    this.notifyDataAdd(this.dataArray.length - 1);
1049  }
1050}
1051
1052@Entry
1053@Component
1054struct Parent {
1055  private data: MyDataSource = new MyDataSource();
1056
1057  aboutToAppear(): void {
1058    for (let i = 0; i < 100; i++) {
1059      this.data.pushData(i.toString());
1060    }
1061  }
1062
1063  build() {
1064    Row() {
1065      List() {
1066        LazyForEach(this.data, (item: string) => {
1067          ListItem() {
1068            Text(item.toString())
1069              .fontSize(16)
1070              .textAlign(TextAlign.Center)
1071              .size({ height: 100, width: '100%' })
1072          }.margin(10)
1073          .borderRadius(10)
1074          .backgroundColor('#FFFFFFFF')
1075        }, (item: string) => item)
1076          .onMove((from: number, to: number) => {
1077            this.data.moveDataWithoutNotify(from, to);
1078          })
1079      }
1080      .width('100%')
1081      .height('100%')
1082      .backgroundColor('#FFDCDCDC')
1083    }
1084  }
1085}
1086```
1087
1088**图12**  LazyForEach拖拽排序效果图
1089![LazyForEach-Drag-Sort](./figures/LazyForEach-Drag-Sort.gif)
1090
1091## 常见问题
1092
1093### 渲染结果非预期
1094
1095```ts
1096/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
1097
1098class MyDataSource extends BasicDataSource {
1099  private dataArray: string[] = [];
1100
1101  public totalCount(): number {
1102    return this.dataArray.length;
1103  }
1104
1105  public getData(index: number): string {
1106    return this.dataArray[index];
1107  }
1108
1109  public pushData(data: string): void {
1110    this.dataArray.push(data);
1111    this.notifyDataAdd(this.dataArray.length - 1);
1112  }
1113
1114  public deleteData(index: number): void {
1115    this.dataArray.splice(index, 1);
1116    this.notifyDataDelete(index);
1117  }
1118}
1119
1120@Entry
1121@Component
1122struct MyComponent {
1123  private data: MyDataSource = new MyDataSource();
1124
1125  aboutToAppear() {
1126    for (let i = 0; i <= 20; i++) {
1127      this.data.pushData(`Hello ${i}`);
1128    }
1129  }
1130
1131  build() {
1132    List({ space: 3 }) {
1133      LazyForEach(this.data, (item: string, index: number) => {
1134        ListItem() {
1135          Row() {
1136            Text(item).fontSize(50)
1137              .onAppear(() => {
1138                console.info(`appear: ${item}`);
1139              })
1140          }.margin({ left: 10, right: 10 })
1141        }
1142        .onClick(() => {
1143          // 点击删除子组件
1144          this.data.deleteData(index);
1145        })
1146      }, (item: string) => item)
1147    }.cachedCount(5)
1148  }
1149}
1150```
1151
1152**图13**  LazyForEach删除数据非预期
1153![LazyForEach-Render-Not-Expected](./figures/LazyForEach-Render-Not-Expected.gif)
1154
1155多次点击子组件时,发现删除的不一定是点击的那个子组件。原因在于删除某个子组件后,该子组件之后的数据项的`index`应减1,但实际后续数据项对应的子组件仍使用最初分配的`index`,`itemGenerator`中的`index`未更新,导致删除结果与预期不符。
1156
1157修复代码如下。
1158
1159```ts
1160/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
1161
1162class MyDataSource extends BasicDataSource {
1163  private dataArray: string[] = [];
1164
1165  public totalCount(): number {
1166    return this.dataArray.length;
1167  }
1168
1169  public getData(index: number): string {
1170    return this.dataArray[index];
1171  }
1172
1173  public pushData(data: string): void {
1174    this.dataArray.push(data);
1175    this.notifyDataAdd(this.dataArray.length - 1);
1176  }
1177
1178  public deleteData(index: number): void {
1179    this.dataArray.splice(index, 1);
1180    this.notifyDataDelete(index);
1181  }
1182
1183  public reloadData(): void {
1184    this.notifyDataReload();
1185  }
1186}
1187
1188@Entry
1189@Component
1190struct MyComponent {
1191  private data: MyDataSource = new MyDataSource();
1192
1193  aboutToAppear() {
1194    for (let i = 0; i <= 20; i++) {
1195      this.data.pushData(`Hello ${i}`);
1196    }
1197  }
1198
1199  build() {
1200    List({ space: 3 }) {
1201      LazyForEach(this.data, (item: string, index: number) => {
1202        ListItem() {
1203          Row() {
1204            Text(item).fontSize(50)
1205              .onAppear(() => {
1206                console.info(`appear: ${item}`);
1207              })
1208          }.margin({ left: 10, right: 10 })
1209        }
1210        .onClick(() => {
1211          // 点击删除子组件
1212          this.data.deleteData(index);
1213          // 重置所有子组件的index索引
1214          this.data.reloadData();
1215        })
1216      }, (item: string, index: number) => item + index.toString())
1217    }.cachedCount(5)
1218  }
1219}
1220```
1221
1222在删除一个数据项后调用`reloadData`方法,重建后面的数据项,以达到更新`index`索引的目的。要保证`reloadData`方法重建数据项,必须保证数据项能生成新的key。这里用了`item + index.toString()`保证被删除数据项后面的数据项都被重建。如果用`item + Date.now().toString()`替代,那么所有数据项都生成新的key,导致所有数据项都被重建。这种方法,效果是一样的,只是性能略差。
1223
1224**图14**  修复LazyForEach删除数据非预期
1225![LazyForEach-Render-Not-Expected-Repair](./figures/LazyForEach-Render-Not-Expected-Repair.gif)
1226
1227### 重渲染时图片闪烁
1228
1229```ts
1230/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
1231
1232class MyDataSource extends BasicDataSource {
1233  private dataArray: StringData[] = [];
1234
1235  public totalCount(): number {
1236    return this.dataArray.length;
1237  }
1238
1239  public getData(index: number): StringData {
1240    return this.dataArray[index];
1241  }
1242
1243  public pushData(data: StringData): void {
1244    this.dataArray.push(data);
1245    this.notifyDataAdd(this.dataArray.length - 1);
1246  }
1247
1248  public reloadData(): void {
1249    this.notifyDataReload();
1250  }
1251}
1252
1253class StringData {
1254  message: string;
1255  imgSrc: Resource;
1256
1257  constructor(message: string, imgSrc: Resource) {
1258    this.message = message;
1259    this.imgSrc = imgSrc;
1260  }
1261}
1262
1263@Entry
1264@Component
1265struct MyComponent {
1266  private moved: number[] = [];
1267  private data: MyDataSource = new MyDataSource();
1268
1269  aboutToAppear() {
1270    for (let i = 0; i <= 20; i++) {
1271      // 此处'app.media.img'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
1272      this.data.pushData(new StringData(`Hello ${i}`, $r('app.media.img')));
1273    }
1274  }
1275
1276  build() {
1277    List({ space: 3 }) {
1278      LazyForEach(this.data, (item: StringData, index: number) => {
1279        ListItem() {
1280          Column() {
1281            Text(item.message).fontSize(50)
1282              .onAppear(() => {
1283                console.info(`appear: ${item.message}`);
1284              })
1285            Image(item.imgSrc)
1286              .width(500)
1287              .height(200)
1288          }.margin({ left: 10, right: 10 })
1289        }
1290        .onClick(() => {
1291          item.message += '00';
1292          this.data.reloadData();
1293        })
1294      }, (item: StringData, index: number) => item.message)
1295    }.cachedCount(5)
1296  }
1297}
1298```
1299
1300**图15**  LazyForEach仅改变文字但是图片闪烁问题
1301![LazyForEach-Image-Flush](./figures/LazyForEach-Image-Flush.gif)
1302
1303单击`ListItem`子组件时,只改变了数据项的`message`属性,但`LazyForEach`的刷新机制会导致整个`ListItem`被重建。由于`Image`组件异步刷新,视觉上图片会闪烁。解决方法是使用`@ObjectLink`和`@Observed`单独刷新子组件`Text`。
1304
1305修复代码如下。
1306
1307```ts
1308/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
1309
1310class MyDataSource extends BasicDataSource {
1311  private dataArray: StringData[] = [];
1312
1313  public totalCount(): number {
1314    return this.dataArray.length;
1315  }
1316
1317  public getData(index: number): StringData {
1318    return this.dataArray[index];
1319  }
1320
1321  public pushData(data: StringData): void {
1322    this.dataArray.push(data);
1323    this.notifyDataAdd(this.dataArray.length - 1);
1324  }
1325}
1326
1327// @Observed类装饰器 和 @ObjectLink 用于在涉及嵌套对象或数组的场景中进行双向数据同步
1328@Observed
1329class StringData {
1330  message: string;
1331  imgSrc: Resource;
1332
1333  constructor(message: string, imgSrc: Resource) {
1334    this.message = message;
1335    this.imgSrc = imgSrc;
1336  }
1337}
1338
1339@Entry
1340@Component
1341struct MyComponent {
1342  private data: MyDataSource = new MyDataSource();
1343
1344  aboutToAppear() {
1345    for (let i = 0; i <= 20; i++) {
1346      // 此处'app.media.img'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
1347      this.data.pushData(new StringData(`Hello ${i}`, $r('app.media.img')));
1348    }
1349  }
1350
1351  build() {
1352    List({ space: 3 }) {
1353      LazyForEach(this.data, (item: StringData, index: number) => {
1354        ListItem() {
1355          ChildComponent({ data: item })
1356        }
1357        .onClick(() => {
1358          item.message += '0';
1359        })
1360      }, (item: StringData, index: number) => index.toString())
1361    }.cachedCount(5)
1362  }
1363}
1364
1365@Component
1366struct ChildComponent {
1367  // 用状态变量来驱动UI刷新,而不是通过Lazyforeach的api来驱动UI刷新
1368  @ObjectLink data: StringData;
1369
1370  build() {
1371    Column() {
1372      Text(this.data.message).fontSize(50)
1373        .onAppear(() => {
1374          console.info(`appear: ${this.data.message}`);
1375        })
1376      Image(this.data.imgSrc)
1377        .width(500)
1378        .height(200)
1379    }.margin({ left: 10, right: 10 })
1380  }
1381}
1382```
1383
1384**图16**  修复LazyForEach仅改变文字但是图片闪烁问题
1385![LazyForEach-Image-Flush-Repair](./figures/LazyForEach-Image-Flush-Repair.gif)
1386
1387### @ObjectLink属性变化UI未更新
1388
1389```ts
1390/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
1391
1392class MyDataSource extends BasicDataSource {
1393  private dataArray: StringData[] = [];
1394
1395  public totalCount(): number {
1396    return this.dataArray.length;
1397  }
1398
1399  public getData(index: number): StringData {
1400    return this.dataArray[index];
1401  }
1402
1403  public pushData(data: StringData): void {
1404    this.dataArray.push(data);
1405    this.notifyDataAdd(this.dataArray.length - 1);
1406  }
1407}
1408
1409@Observed
1410class StringData {
1411  message: NestedString;
1412
1413  constructor(message: NestedString) {
1414    this.message = message;
1415  }
1416}
1417
1418@Observed
1419class NestedString {
1420  message: string;
1421
1422  constructor(message: string) {
1423    this.message = message;
1424  }
1425}
1426
1427@Entry
1428@Component
1429struct MyComponent {
1430  private moved: number[] = [];
1431  private data: MyDataSource = new MyDataSource();
1432
1433  aboutToAppear() {
1434    for (let i = 0; i <= 20; i++) {
1435      this.data.pushData(new StringData(new NestedString(`Hello ${i}`)));
1436    }
1437  }
1438
1439  build() {
1440    List({ space: 3 }) {
1441      LazyForEach(this.data, (item: StringData, index: number) => {
1442        ListItem() {
1443          ChildComponent({ data: item })
1444        }
1445        .onClick(() => {
1446          item.message.message += '0';
1447        })
1448      }, (item: StringData, index: number) => item.message.message + index.toString())
1449    }.cachedCount(5)
1450  }
1451}
1452
1453@Component
1454struct ChildComponent {
1455  @ObjectLink data: StringData;
1456
1457  build() {
1458    Row() {
1459      Text(this.data.message.message).fontSize(50)
1460        .onAppear(() => {
1461          console.info(`appear: ${this.data.message.message}`);
1462        })
1463    }.margin({ left: 10, right: 10 })
1464  }
1465}
1466```
1467
1468**图17**  ObjectLink属性变化后UI未更新
1469![LazyForEach-ObjectLink-NotRenderUI](./figures/LazyForEach-ObjectLink-NotRenderUI.gif)
1470
1471@ObjectLink装饰的成员变量仅能监听到其子属性的变化,无法监听深层嵌套属性,因此,只能通过修改子属性来通知组件重新渲染。具体[请查看@ObjectLink与@Observed的详细使用方法和限制条件](./arkts-observed-and-objectlink.md)。
1472
1473修复代码如下。
1474
1475```ts
1476/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
1477
1478class MyDataSource extends BasicDataSource {
1479  private dataArray: StringData[] = [];
1480
1481  public totalCount(): number {
1482    return this.dataArray.length;
1483  }
1484
1485  public getData(index: number): StringData {
1486    return this.dataArray[index];
1487  }
1488
1489  public pushData(data: StringData): void {
1490    this.dataArray.push(data);
1491    this.notifyDataAdd(this.dataArray.length - 1);
1492  }
1493}
1494
1495@Observed
1496class StringData {
1497  message: NestedString;
1498
1499  constructor(message: NestedString) {
1500    this.message = message;
1501  }
1502}
1503
1504@Observed
1505class NestedString {
1506  message: string;
1507
1508  constructor(message: string) {
1509    this.message = message;
1510  }
1511}
1512
1513@Entry
1514@Component
1515struct MyComponent {
1516  private moved: number[] = [];
1517  private data: MyDataSource = new MyDataSource();
1518
1519  aboutToAppear() {
1520    for (let i = 0; i <= 20; i++) {
1521      this.data.pushData(new StringData(new NestedString(`Hello ${i}`)));
1522    }
1523  }
1524
1525  build() {
1526    List({ space: 3 }) {
1527      LazyForEach(this.data, (item: StringData, index: number) => {
1528        ListItem() {
1529          ChildComponent({ data: item })
1530        }
1531        .onClick(() => {
1532          // @ObjectLink装饰的成员变量仅能监听到其子属性的变化,再深入嵌套的属性便无法观测到
1533          item.message = new NestedString(item.message.message + '0');
1534        })
1535      }, (item: StringData, index: number) => item.message.message + index.toString())
1536    }.cachedCount(5)
1537  }
1538}
1539
1540@Component
1541struct ChildComponent {
1542  @ObjectLink data: StringData;
1543
1544  build() {
1545    Row() {
1546      Text(this.data.message.message).fontSize(50)
1547        .onAppear(() => {
1548          console.info(`appear: ${this.data.message.message}`);
1549        })
1550    }.margin({ left: 10, right: 10 })
1551  }
1552}
1553```
1554
1555**图18**  修复ObjectLink属性变化后UI更新
1556![LazyForEach-ObjectLink-NotRenderUI-Repair](./figures/LazyForEach-ObjectLink-NotRenderUI-Repair.gif)
1557
1558### 在List内使用屏幕闪烁
1559在List的onScrollIndex方法中调用onDataReloaded可能会导致屏幕闪烁。
1560
1561```ts
1562/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
1563
1564class MyDataSource extends BasicDataSource {
1565  private dataArray: string[] = [];
1566
1567  public totalCount(): number {
1568    return this.dataArray.length;
1569  }
1570
1571  public getData(index: number): string {
1572    return this.dataArray[index];
1573  }
1574
1575  public pushData(data: string): void {
1576    this.dataArray.push(data);
1577    this.notifyDataAdd(this.dataArray.length - 1);
1578  }
1579
1580  operateData(): void {
1581    const totalCount = this.dataArray.length;
1582    const batch = 5;
1583    for (let i = totalCount; i < totalCount + batch; i++) {
1584      this.dataArray.push(`Hello ${i}`);
1585    }
1586    this.notifyDataReload();
1587  }
1588}
1589
1590@Entry
1591@Component
1592struct MyComponent {
1593  private moved: number[] = [];
1594  private data: MyDataSource = new MyDataSource();
1595
1596  aboutToAppear() {
1597    for (let i = 0; i <= 10; i++) {
1598      this.data.pushData(`Hello ${i}`);
1599    }
1600  }
1601
1602  build() {
1603    List({ space: 3 }) {
1604      LazyForEach(this.data, (item: string, index: number) => {
1605        ListItem() {
1606          Row() {
1607            Text(item)
1608              .width('100%')
1609              .height(80)
1610              .backgroundColor(Color.Gray)
1611              .onAppear(() => {
1612                console.info(`appear: ${item}`);
1613              })
1614          }.margin({ left: 10, right: 10 })
1615        }
1616      }, (item: string) => item)
1617    }.cachedCount(10)
1618    .onScrollIndex((start, end, center) => {
1619      if (end === this.data.totalCount() - 1) {
1620        console.info('scroll to end');
1621        this.data.operateData();
1622      }
1623    })
1624  }
1625}
1626```
1627
1628**图19**  当List下拉到底时,屏幕闪烁
1629![LazyForEach-Screen-Flicker](./figures/LazyForEach-Screen-Flicker.gif)
1630
1631使用`onDatasetChange`代替`onDataReloaded`,不仅可以修复闪屏问题,还能提升加载性能。
1632
1633```ts
1634/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
1635
1636class MyDataSource extends BasicDataSource {
1637  private dataArray: string[] = [];
1638
1639  public totalCount(): number {
1640    return this.dataArray.length;
1641  }
1642
1643  public getData(index: number): string {
1644    return this.dataArray[index];
1645  }
1646
1647  public pushData(data: string): void {
1648    this.dataArray.push(data);
1649    this.notifyDataAdd(this.dataArray.length - 1);
1650  }
1651
1652  operateData(): void {
1653    const totalCount = this.dataArray.length;
1654    const batch = 5;
1655    for (let i = totalCount; i < totalCount + batch; i++) {
1656      this.dataArray.push(`Hello ${i}`);
1657    }
1658    // 替换 notifyDataReload
1659    this.notifyDatasetChange([{ type: DataOperationType.ADD, index: totalCount - 1, count: batch }]);
1660  }
1661}
1662
1663@Entry
1664@Component
1665struct MyComponent {
1666  private moved: number[] = [];
1667  private data: MyDataSource = new MyDataSource();
1668
1669  aboutToAppear() {
1670    for (let i = 0; i <= 10; i++) {
1671      this.data.pushData(`Hello ${i}`);
1672    }
1673  }
1674
1675  build() {
1676    List({ space: 3 }) {
1677      LazyForEach(this.data, (item: string, index: number) => {
1678        ListItem() {
1679          Row() {
1680            Text(item)
1681              .width('100%')
1682              .height(80)
1683              .backgroundColor(Color.Gray)
1684              .onAppear(() => {
1685                console.info(`appear: ${item}`);
1686              })
1687          }.margin({ left: 10, right: 10 })
1688        }
1689      }, (item: string) => item)
1690    }.cachedCount(10)
1691    .onScrollIndex((start, end, center) => {
1692      if (end === this.data.totalCount() - 1) {
1693        console.info('scroll to end');
1694        this.data.operateData();
1695      }
1696    })
1697  }
1698}
1699```
1700
1701**图20**  修复后,当List下拉到底时,屏幕不闪烁
1702![LazyForEach-Screen-Flicker-Repair](./figures/LazyForEach-Screen-Flicker-Repair.gif)
1703
1704### 组件复用渲染异常
1705
1706`@Reusable`与[\@ComponentV2](./arkts-new-componentV2.md)混用会导致组件渲染异常。
1707
1708```ts
1709/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 **/
1710
1711class MyDataSource extends BasicDataSource {
1712  private dataArray: StringData[] = [];
1713
1714  public totalCount(): number {
1715    return this.dataArray.length;
1716  }
1717
1718  public getData(index: number): StringData {
1719    return this.dataArray[index];
1720  }
1721
1722  public pushData(data: StringData): void {
1723    this.dataArray.push(data);
1724    this.notifyDataAdd(this.dataArray.length - 1);
1725  }
1726}
1727
1728
1729class StringData {
1730  message: string;
1731
1732  constructor(message: string) {
1733    this.message = message;
1734  }
1735}
1736
1737@Entry
1738@ComponentV2
1739struct MyComponent {
1740  data: MyDataSource = new MyDataSource();
1741
1742  aboutToAppear() {
1743    for (let i = 0; i <= 30; i++) {
1744      this.data.pushData(new StringData(`Hello${i}`));
1745    }
1746  }
1747
1748  build() {
1749    List({ space: 3 }) {
1750      LazyForEach(this.data, (item: StringData, index: number) => {
1751        ListItem() {
1752          ChildComponent({ data: item })
1753            .onAppear(() => {
1754              console.info(`onAppear: ${item.message}`);
1755            })
1756        }
1757      }, (item: StringData, index: number) => index.toString())
1758    }.cachedCount(5)
1759  }
1760}
1761
1762@Reusable
1763@Component
1764struct ChildComponent {
1765  @State data: StringData = new StringData('');
1766
1767  aboutToAppear(): void {
1768    console.info(`aboutToAppear: ${this.data.message}`);
1769  }
1770
1771  aboutToRecycle(): void {
1772    console.info(`aboutToRecycle: ${this.data.message}`);
1773  }
1774
1775  // 对复用的组件进行数据更新
1776  aboutToReuse(params: Record<string, ESObject>): void {
1777    this.data = params.data as StringData;
1778    console.info(`aboutToReuse: ${this.data.message}`);
1779  }
1780
1781  build() {
1782    Row() {
1783      Text(this.data.message).fontSize(50)
1784    }
1785  }
1786}
1787```
1788
1789反例中,在`@ComponentV2`装饰的组件`MyComponent`中,`LazyForEach`列表使用了`@Reusable`装饰的组件`ChildComponent`,导致组件渲染失败。从日志中可以看到,组件触发了`onAppear`,但没有触发`aboutToAppear`。
1790
1791将`@ComponentV2`修改为[\@Component](./arkts-create-custom-components.md#component)可以修复渲染异常。修复后,当滑动事件触发组件节点下树时,对应的可复用组件`ChildComponent`会被加入复用缓存,而非被销毁,并触发`aboutToRecycle`事件,打印日志信息。当列表滑动,出现新节点时,会将可复用的组件从复用缓存中重新加入到节点树,触发`aboutToReuse`刷新组件数据,并打印日志信息。
1792
1793### 组件不刷新
1794
1795开发者需要定义合适的键值生成函数,返回与目标数据相关联的键值。目标数据发生改变时,LazyForEach识别到键值改变才会刷新对应组件。
1796
1797```ts
1798/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
1799
1800class MyDataSource extends BasicDataSource {
1801  private dataArray: string[] = [];
1802
1803  public totalCount(): number {
1804    return this.dataArray.length;
1805  }
1806
1807  public getData(index: number): string {
1808    return this.dataArray[index];
1809  }
1810
1811  public pushData(data: string): void {
1812    this.dataArray.push(data);
1813    this.notifyDataAdd(this.dataArray.length - 1);
1814  }
1815
1816  public updateAllData(): void {
1817    this.dataArray = this.dataArray.map((item: string) => item + `!`);
1818    this.notifyDataReload();
1819  }
1820}
1821
1822@Entry
1823@Component
1824struct MyComponent {
1825  private data: MyDataSource = new MyDataSource();
1826
1827  aboutToAppear() {
1828    for (let i = 0; i <= 20; i++) {
1829      this.data.pushData(`Hello ${i}`);
1830    }
1831  }
1832
1833  build() {
1834    Column() {
1835      Button(`update all`)
1836        .onClick(() => {
1837          this.data.updateAllData();
1838        })
1839      List({ space: 3 }) {
1840        LazyForEach(this.data, (item: string) => {
1841          ListItem() {
1842            Text(item).fontSize(50)
1843          }
1844        })
1845      }.cachedCount(5)
1846    }
1847  }
1848}
1849```
1850
1851**图21**  点击按钮更新数据,组件不会刷新
1852![LazyForEach-Refresh-Not-Expected](./figures/LazyForEach-Refresh-Not-Expected.gif)
1853
1854LazyForEach依赖生成的键值判断是否刷新子组件,如果更新的数据没有改变键值(如示例中开发者没有定义键值生成函数,此时键值仅与组件索引index有关,更新数据时键值不变),则LazyForEach不会刷新对应组件。
1855
1856```ts
1857LazyForEach(this.data, (item: string) => {
1858  ListItem() {
1859    Text(item).fontSize(50)
1860  }
1861}, (item: string) => item) // 定义键值生成函数
1862```
1863
1864**图22**  定义键值生成函数后,点击按钮更新数据,组件刷新
1865![LazyForEach-Refresh-Not-Expected-Repair](./figures/LazyForEach-Refresh-Not-Expected-Repair.gif)
1866
1867### 懒加载失效
1868
1869支持数据懒加载的父组件基于自身和子组件的高度或宽度计算可视范围内应布局的子节点数量,高度或宽度的缺失会导致部分场景懒加载失效。如下示例,在纵向布局中,首次渲染时子组件的高度缺失,所有数据项对应组件都会被创建。
1870
1871```ts
1872/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 **/
1873
1874class MyDataSource extends BasicDataSource {
1875  public dataArray: string[] = [];
1876
1877  public totalCount(): number {
1878    return this.dataArray.length;
1879  }
1880
1881  public getData(index: number): string {
1882    return this.dataArray[index];
1883  }
1884
1885  public pushData(data: string): void {
1886    this.dataArray.push(data);
1887    this.notifyDataAdd(this.dataArray.length - 1);
1888  }
1889}
1890
1891@Entry
1892@Component
1893struct MyComponent {
1894  private data: MyDataSource = new MyDataSource();
1895
1896  aboutToAppear() {
1897    for (let i = 0; i <= 100; i++) {
1898      this.data.pushData(``);
1899    }
1900  }
1901
1902  build() {
1903    List() {
1904      LazyForEach(this.data, (item: string, index: number) => {
1905        ChildComponent({ message: item, index: index })
1906        // 子组件未设置默认高度,首次渲染时所有数据项对应组件都被创建
1907        // .height(60)
1908      }, (item: string, index: number) => item + index)
1909    }
1910    .cachedCount(2)
1911  }
1912}
1913
1914@Component
1915struct ChildComponent {
1916  message: string = ``;
1917  index: number = -1;
1918
1919  aboutToAppear(): void {
1920    console.info(`about to appear ${this.index}`);
1921  }
1922
1923  build() {
1924    Text(this.message).fontSize(50)
1925  }
1926}
1927```
1928
1929上述示例由于子组件`ChildComponent`的变量`message`初始值为空字符串,导致其内部的`Text`组件高度为 0,同时子组件未显式设置默认高度(如`.height(60)`),因此在首次渲染时所有子组件的高度均被计算为0。父组件`List`在基于高度计算可视范围时,判断所有子组件均位于可视区域内,导致懒加载机制失效,最终触发了全部数据项对应组件的创建(可通过日志观察到所有`about to appear`打印)。
1930
1931为子组件设置默认高度,确保父组件能正确计算可视范围,从而恢复此场景下懒加载功能。
1932
1933```ts
1934List() {
1935  LazyForEach(this.data, (item: string, index: number) => {
1936    ChildComponent({ message: item, index: index })
1937      // 设置子组件默认高度,首次渲染懒加载生效
1938      .height(60)
1939  }, (item: string, index: number) => item + index)
1940}
1941.cachedCount(2)
1942```
1943
1944## BasicDataSource示例代码
1945
1946### string类型数组的BasicDataSource代码
1947
1948```ts
1949// BasicDataSource实现了IDataSource接口,用于管理listener监听,以及通知LazyForEach数据更新
1950class BasicDataSource implements IDataSource {
1951  private listeners: DataChangeListener[] = [];
1952  private originDataArray: string[] = [];
1953
1954  public totalCount(): number {
1955    return this.originDataArray.length;
1956  }
1957
1958  public getData(index: number): string {
1959    return this.originDataArray[index];
1960  }
1961
1962  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
1963  registerDataChangeListener(listener: DataChangeListener): void {
1964    if (this.listeners.indexOf(listener) < 0) {
1965      console.info('add listener');
1966      this.listeners.push(listener);
1967    }
1968  }
1969
1970  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
1971  unregisterDataChangeListener(listener: DataChangeListener): void {
1972    const pos = this.listeners.indexOf(listener);
1973    if (pos >= 0) {
1974      console.info('remove listener');
1975      this.listeners.splice(pos, 1);
1976    }
1977  }
1978
1979  // 通知LazyForEach组件需要重载所有子组件
1980  notifyDataReload(): void {
1981    this.listeners.forEach(listener => {
1982      listener.onDataReloaded();
1983    });
1984  }
1985
1986  // 通知LazyForEach组件需要在index对应索引处添加子组件
1987  notifyDataAdd(index: number): void {
1988    this.listeners.forEach(listener => {
1989      listener.onDataAdd(index);
1990      // 写法2:listener.onDatasetChange([{type: DataOperationType.ADD, index: index}]);
1991    });
1992  }
1993
1994  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
1995  notifyDataChange(index: number): void {
1996    this.listeners.forEach(listener => {
1997      listener.onDataChange(index);
1998      // 写法2:listener.onDatasetChange([{type: DataOperationType.CHANGE, index: index}]);
1999    });
2000  }
2001
2002  // 通知LazyForEach组件需要在index对应索引处删除该子组件
2003  notifyDataDelete(index: number): void {
2004    this.listeners.forEach(listener => {
2005      listener.onDataDelete(index);
2006      // 写法2:listener.onDatasetChange([{type: DataOperationType.DELETE, index: index}]);
2007    });
2008  }
2009
2010  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换
2011  notifyDataMove(from: number, to: number): void {
2012    this.listeners.forEach(listener => {
2013      listener.onDataMove(from, to);
2014      // 写法2:listener.onDatasetChange(
2015      //         [{type: DataOperationType.EXCHANGE, index: {start: from, end: to}}]);
2016    });
2017  }
2018
2019  notifyDatasetChange(operations: DataOperation[]): void {
2020    this.listeners.forEach(listener => {
2021      listener.onDatasetChange(operations);
2022    });
2023  }
2024}
2025```
2026
2027### StringData类型数组的BasicDataSource代码
2028
2029```ts
2030class BasicDataSource implements IDataSource {
2031  private listeners: DataChangeListener[] = [];
2032  private originDataArray: StringData[] = [];
2033
2034  public totalCount(): number {
2035    return this.originDataArray.length;
2036  }
2037
2038  public getData(index: number): StringData {
2039    return this.originDataArray[index];
2040  }
2041
2042  registerDataChangeListener(listener: DataChangeListener): void {
2043    if (this.listeners.indexOf(listener) < 0) {
2044      console.info('add listener');
2045      this.listeners.push(listener);
2046    }
2047  }
2048
2049  unregisterDataChangeListener(listener: DataChangeListener): void {
2050    const pos = this.listeners.indexOf(listener);
2051    if (pos >= 0) {
2052      console.info('remove listener');
2053      this.listeners.splice(pos, 1);
2054    }
2055  }
2056
2057  notifyDataReload(): void {
2058    this.listeners.forEach(listener => {
2059      listener.onDataReloaded();
2060    });
2061  }
2062
2063  notifyDataAdd(index: number): void {
2064    this.listeners.forEach(listener => {
2065      listener.onDataAdd(index);
2066    });
2067  }
2068
2069  notifyDataChange(index: number): void {
2070    this.listeners.forEach(listener => {
2071      listener.onDataChange(index);
2072    });
2073  }
2074
2075  notifyDataDelete(index: number): void {
2076    this.listeners.forEach(listener => {
2077      listener.onDataDelete(index);
2078    });
2079  }
2080
2081  notifyDataMove(from: number, to: number): void {
2082    this.listeners.forEach(listener => {
2083      listener.onDataMove(from, to);
2084    });
2085  }
2086
2087  notifyDatasetChange(operations: DataOperation[]): void {
2088    this.listeners.forEach(listener => {
2089      listener.onDatasetChange(operations);
2090    });
2091  }
2092}
2093```