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