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