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