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