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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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```