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