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