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