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