1# 组件复用总览 2 3<!--Kit: Common--> 4<!--Subsystem: Demo&Sample--> 5<!--Owner: @mgy917--> 6<!--Designer: @jiangwensai--> 7<!--Tester: @Lyuxin--> 8<!--Adviser: @huipeizi--> 9 10组件复用是优化用户界面性能,提升应用流畅度的一种核心策略,它通过复用已存在的组件节点而非创建新的节点,大幅度降低了因频繁创建与销毁组件带来的性能损耗,从而确保UI线程的流畅性与响应速度。组件复用针对的是自定义组件,只要发生了相同自定义组件销毁和再创建的场景,都可以使用组件复用。 11 12本文系统地描述了六种复用类型及其应用场景,帮助开发者更好地理解和实施组件复用策略以优化应用性能。 13 14关于组件复用的原理机制可以参考资料[组件复用原理机制](./component_recycle_case.md#组件复用原理机制),便于理解本文内容。 15 16## 复用类型总览 17 18|复用类型|描述|复用思路|参考文档| 19|:--:|--|--|--| 20|**标准型**|复用组件之间布局完全相同|标准复用|[组件复用实践](./component-recycle.md)| 21|**有限变化型**|复用组件之间有不同,但是类型有限|使用reuseId或者独立成两个自定义组件|[组件复用性能优化指导](./component_recycle_case.md)| 22|**组合型**|复用组件之间有不同,情况非常多,但是拥有共同的子组件|将复用组件改为Builder,让内部子组件相互之间复用|[组合型组件复用指导](#组合型)| 23|**全局型**|组件可在不同的父组件中复用,并且不适合使用@Builder|使用BuilderNode自定义复用组件池,在整个应用中自由流转|[全局自定义组件复用实现](./node_custom_component_reusable_pool.md)| 24|**嵌套型**|复用组件的子组件的子组件存在差异|采用化归思想将嵌套问题转化为上面四种标准类型来解决|/| 25|**无法复用型**|组件之间差别很大,规律性不强,子组件也不相同|不建议使用组件复用|/| 26 27## 各个复用类型详解 28 29下文为了方便描述,以一个滑动列表的场景为例,将要复用的自定义组件如ListItem的内容组件,叫做**复用组件**,把它子级的自定义组件叫做**子组件**,把**复用组件**上层的自定义组件叫做**父组件**。为了更直观,下面每一种复用类型都会通过简易的图形展示组件的布局方式,并且为了便于分辨,布局相同的子组件使用同一种形状表示。 30 31### 标准型 32 33 34 35这是一个标准的组件复用场景,一个滚动容器内的复用组件布局相同,只有数据不同。这种类型的组件复用可以直接参考资料[组件复用实践](./component-recycle.md)。 36 37**应用场景案例** 38 39 40 41### 有限变化型 42 43 44 45这种类型中复用组件之间存在不同,但是类型有限。如上图所示,容器内的复用组件内部的子组件不一样,但可总结为两种类型,类型 1由三个子组件 A 进行布局拼接而成,类型 2由子组件 B、子组件 C 和子组件 D 进行布局拼接而成。 46 47此时存在以下两种应对措施: 48 49- **类型1和类型2业务逻辑不同**:建议将两种类型的组件使用两个不同的自定义组件,分别进行复用。此时组件复用池内的状态如下图所示,复用组件 1 和复用组件 2 处于不同的复用 list 中。 50 51 52 53实现方式可参考以下示例代码: 54 55```typescript 56class MyDataSource implements IDataSource { 57 // ... 58} 59 60@Entry 61@Component 62struct Index { 63 private data: MyDataSource = new MyDataSource(); 64 65 aboutToAppear() { 66 for (let i = 0; i < 1000; i++) { 67 this.data.pushData(i); 68 } 69 } 70 71 build() { 72 Column() { 73 List({ space: 10 }) { 74 LazyForEach(this.data, (item: number) => { 75 ListItem() { 76 if (item % 2 === 0) { 77 ReusableComponentOne({ item: item.toString() }) 78 } else { 79 ReusableComponentTwo({ item: item.toString() }) 80 } 81 } 82 .backgroundColor(Color.Orange) 83 .width('100%') 84 }, (item: number) => item.toString()) 85 } 86 .cachedCount(2) 87 } 88 } 89} 90 91@Reusable 92@Component 93struct ReusableComponentOne { 94 @State item: string = ''; 95 96 aboutToReuse(params: ESObject) { 97 this.item = params.item; 98 } 99 100 build() { 101 Column() { 102 Text(`Item ${this.item} ReusableComponentOne`) 103 .fontSize(20) 104 .margin({ left: 10 }) 105 }.margin({ left: 10, right: 10 }) 106 } 107} 108 109@Reusable 110@Component 111struct ReusableComponentTwo { 112 @State item: string = ''; 113 114 aboutToReuse(params: ESObject) { 115 this.item = params.item; 116 } 117 118 build() { 119 Column() { 120 Text(`Item ${this.item} ReusableComponentTwo`) 121 .fontSize(20) 122 .margin({ left: 10 }) 123 }.margin({ left: 10, right: 10 }) 124 } 125} 126``` 127 128- **类型1和类型2布局不同,但是很多业务逻辑相同**:在这种情况下,如果将组件分为两个自定义组件进行复用,会存在代码冗余问题。根据系统组件复用原理可知,复用组件是依据 reuseId 来区分复用缓存池的,而自定义组件的名称就是默认的 reuseId。因此,为复用组件显式设置两个 reuseId 与使用两个自定义组件进行复用,对于 ArkUI 而言,复用逻辑完全相同。此时组件复用池内的状态如下图所示。 129 130 131 132具体实现方式可以参考以下示例: 133 134```typescript 135class MyDataSource implements IDataSource { 136 // ... 137} 138 139@Entry 140@Component 141struct Index { 142 private data: MyDataSource = new MyDataSource(); 143 144 aboutToAppear() { 145 for (let i = 0; i < 1000; i++) { 146 this.data.pushData(i); 147 } 148 } 149 150 build() { 151 Column() { 152 List({ space: 10 }) { 153 LazyForEach(this.data, (item: number) => { 154 ListItem() { 155 ReusableComponent({ item: item }) 156 .reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo') 157 } 158 .backgroundColor(Color.Orange) 159 .width('100%') 160 }, (item: number) => item.toString()) 161 } 162 .cachedCount(2) 163 } 164 } 165} 166 167@Reusable 168@Component 169struct ReusableComponent { 170 @State item: number = 0; 171 172 aboutToReuse(params: ESObject) { 173 this.item = params.item; 174 } 175 176 build() { 177 Column() { 178 if (this.item % 2 === 0) { 179 Text(`Item ${this.item} ReusableComponentOne`) 180 .fontSize(20) 181 .margin({ left: 10 }) 182 } else { 183 Text(`Item ${this.item} ReusableComponentTwo`) 184 .fontSize(20) 185 .margin({ left: 10 }) 186 } 187 }.margin({ left: 10, right: 10 }) 188 } 189} 190``` 191 192**应用场景案例** 193 194 195 196### 组合型 197 198 199 200这种类型中复用组件之间存在不同,并且情况非常多,但拥有共同的子组件。如果使用有限变化型的组件复用方式,将所有类型的复用组件写成自定义组件分别复用,那么不同复用组件的复用 list 中相同的子组件之间不能互相复用。对此可以将复用组件转变为 Builder 函数,使复用组件内部共同的子组件的缓存池在父组件上共享。此时组件复用池内的状态如下图所示。 201 202 203 204**反例** 205 206下面是使用有限变化型组件复用的一段示例代码: 207 208```typescript 209class MyDataSource implements IDataSource { 210 // ... 211} 212 213@Entry 214@Component 215struct MyComponent { 216 private data: MyDataSource = new MyDataSource(); 217 218 aboutToAppear() { 219 for (let i = 0; i < 1000; i++) { 220 this.data.pushData(i.toString()); 221 } 222 } 223 224 build() { 225 List({ space: 40 }) { 226 LazyForEach(this.data, (item: string, index: number) => { 227 ListItem() { 228 if (index % 3 === 0) { 229 ReusableComponentOne({ item: item }) 230 } else if (index % 5 === 0) { 231 ReusableComponentTwo({ item: item }) 232 } else { 233 ReusableComponentThree({ item: item }) 234 } 235 } 236 .backgroundColor('#cccccc') 237 .width('100%') 238 .onAppear(()=>{ 239 console.info(`ListItem ${index} onAppear`); 240 }) 241 }) 242 } 243 .width('100%') 244 .height('100%') 245 .cachedCount(0) 246 } 247} 248 249@Reusable 250@Component 251struct ReusableComponentOne { 252 @State item: string = ''; 253 254 // 组件的生命周期回调,在可复用组件从复用缓存中加入到组件树之前调用 255 aboutToReuse(params: ESObject) { 256 console.info(`ReusableComponentOne ${params.item} Reuse ${this.item}`); 257 this.item = params.item; 258 } 259 260 // 组件的生命周期回调,在可复用组件从组件树上被加入到复用缓存之前调用 261 aboutToRecycle(): void { 262 console.info(`ReusableComponentOne ${this.item} Recycle`); 263 } 264 265 build() { 266 Column() { 267 ChildComponentA({ item: this.item }) 268 ChildComponentB({ item: this.item }) 269 ChildComponentC({ item: this.item }) 270 } 271 } 272} 273 274@Reusable 275@Component 276struct ReusableComponentTwo { 277 @State item: string = ''; 278 279 aboutToReuse(params: ESObject) { 280 console.info(`ReusableComponentTwo ${params.item} Reuse ${this.item}`); 281 this.item = params.item; 282 } 283 284 aboutToRecycle(): void { 285 console.info(`ReusableComponentTwo ${this.item} Recycle`); 286 } 287 288 build() { 289 Column() { 290 ChildComponentA({ item: this.item }) 291 ChildComponentC({ item: this.item }) 292 ChildComponentD({ item: this.item }) 293 } 294 } 295} 296 297@Reusable 298@Component 299struct ReusableComponentThree { 300 @State item: string = ''; 301 302 aboutToReuse(params: ESObject) { 303 console.info(`ReusableComponentThree ${params.item} Reuse ${this.item}`); 304 this.item = params.item; 305 } 306 307 aboutToRecycle(): void { 308 console.info(`ReusableComponentThree ${this.item} Recycle`); 309 } 310 311 build() { 312 Column() { 313 ChildComponentA({ item: this.item }) 314 ChildComponentB({ item: this.item }) 315 ChildComponentD({ item: this.item }) 316 } 317 } 318} 319 320@Component 321struct ChildComponentA { 322 @State item: string = ''; 323 324 aboutToReuse(params: ESObject) { 325 console.info(`ChildComponentA ${params.item} Reuse ${this.item}`); 326 this.item = params.item; 327 } 328 329 aboutToRecycle(): void { 330 console.info(`ChildComponentA ${this.item} Recycle`); 331 } 332 333 build() { 334 Column() { 335 Text(`Item ${this.item} Child Component A`) 336 .fontSize(20) 337 .margin({ left: 10 }) 338 .fontColor(Color.Blue) 339 Grid() { 340 ForEach((new Array(20)).fill(''), (item: string,index: number) => { 341 GridItem() { 342 Image($r('app.media.icon')) 343 .height(20) 344 } 345 }) 346 } 347 .columnsTemplate('1fr 1fr 1fr 1fr 1fr') 348 .rowsTemplate('1fr 1fr 1fr 1fr') 349 .columnsGap(10) 350 .width('90%') 351 .height(160) 352 } 353 .margin({ left: 10, right: 10 }) 354 .backgroundColor(0xFAEEE0) 355 } 356} 357 358@Component 359struct ChildComponentB { 360 @State item: string = ''; 361 362 aboutToReuse(params: ESObject) { 363 this.item = params.item; 364 } 365 366 build() { 367 Row() { 368 Text(`Item ${this.item} Child Component B`) 369 .fontSize(20) 370 .margin({ left: 10 }) 371 .fontColor(Color.Red) 372 }.margin({ left: 10, right: 10 }) 373 } 374} 375 376@Component 377struct ChildComponentC { 378 @State item: string = ''; 379 380 aboutToReuse(params: ESObject) { 381 this.item = params.item; 382 } 383 384 build() { 385 Row() { 386 Text(`Item ${this.item} Child Component C`) 387 .fontSize(20) 388 .margin({ left: 10 }) 389 .fontColor(Color.Green) 390 }.margin({ left: 10, right: 10 }) 391 } 392} 393 394@Component 395struct ChildComponentD { 396 @State item: string = ''; 397 398 aboutToReuse(params: ESObject) { 399 this.item = params.item; 400 } 401 402 build() { 403 Row() { 404 Text(`Item ${this.item} Child Component D`) 405 .fontSize(20) 406 .margin({ left: 10 }) 407 .fontColor(Color.Orange) 408 }.margin({ left: 10, right: 10 }) 409 } 410} 411``` 412 413上述代码中由四个子组件按不同的排列组合组成了三种类型的复用组件。为了方便观察组件的缓存和复用情况,将 List 的 cachedCount 设置为0,并在部分自定义组件的生命周期函数中添加日志输出。其中重点观察子组件 ChildComponentA 的缓存和复用。 414 415示例运行效果图如下: 416 417 418 419从上图可以看到,列表滑动到 ListItem 0 消失时,复用组件 ReusableComponentOne 和它的子组件 ChildComponentA 都加入了复用缓存。继续向上滑动时,由于 ListItem 4 与 ListItem 0 的复用组件不在同一个复用 list,因此 ListItem 4 的复用组件 ReusableComponentThree 和它的子组件依然会全部重新创建,不会复用缓存中的子组件 ChildComponentA。 420 421此时 ListItem 4 中的子组件 ChildComponentA 的重新创建耗时 6ms387μs499ns。 422 423 424 425**正例** 426 427按照组合型的组件复用方式,将上述示例中的三种复用组件转变为 Builder 函数后,内部共同的子组件就处于同一个父组件 MyComponent 下。对这些子组件使用组件复用时,它们的缓存池也会在父组件上共享,节省组件创建时的消耗。 428 429修改后的示例代码: 430 431```typescript 432class MyDataSource implements IDataSource { 433 // ... 434} 435 436@Entry 437@Component 438struct MyComponent { 439 private data: MyDataSource = new MyDataSource(); 440 441 aboutToAppear() { 442 for (let i = 0; i < 1000; i++) { 443 this.data.pushData(i.toString()) 444 } 445 } 446 447 @Builder 448 itemBuilderOne(item: string) { 449 Column() { 450 ChildComponentA({ item: item }) 451 ChildComponentB({ item: item }) 452 ChildComponentC({ item: item }) 453 } 454 } 455 456 @Builder 457 itemBuilderTwo(item: string) { 458 Column() { 459 ChildComponentA({ item: item }) 460 ChildComponentC({ item: item }) 461 ChildComponentD({ item: item }) 462 } 463 } 464 465 @Builder 466 itemBuilderThree(item: string) { 467 Column() { 468 ChildComponentA({ item: item }) 469 ChildComponentB({ item: item }) 470 ChildComponentD({ item: item }) 471 } 472 } 473 474 build() { 475 List({ space: 40 }) { 476 LazyForEach(this.data, (item: string, index: number) => { 477 ListItem() { 478 if (index % 3 === 0) { 479 this.itemBuilderOne(item) 480 } else if (index % 5 === 0) { 481 this.itemBuilderTwo(item) 482 } else { 483 this.itemBuilderThree(item) 484 } 485 } 486 .backgroundColor('#cccccc') 487 .width('100%') 488 .onAppear(() => { 489 console.info(`ListItem ${index} onAppear`); 490 }) 491 }, (item: number) => item.toString()) 492 } 493 .width('100%') 494 .height('100%') 495 .cachedCount(0) 496 } 497} 498 499@Reusable 500@Component 501struct ChildComponentA { 502 @State item: string = ''; 503 504 aboutToReuse(params: ESObject) { 505 console.info(`ChildComponentA ${params.item} Reuse ${this.item}`); 506 this.item = params.item; 507 } 508 509 aboutToRecycle(): void { 510 console.info(`ChildComponentA ${this.item} Recycle`); 511 } 512 513 build() { 514 Column() { 515 Text(`Item ${this.item} Child Component A`) 516 .fontSize(20) 517 .margin({ left: 10 }) 518 .fontColor(Color.Blue) 519 Grid() { 520 ForEach((new Array(20)).fill(''), (item: string,index: number) => { 521 GridItem() { 522 Image($r('app.media.icon')) 523 .height(20) 524 } 525 }) 526 } 527 .columnsTemplate('1fr 1fr 1fr 1fr 1fr') 528 .rowsTemplate('1fr 1fr 1fr 1fr') 529 .columnsGap(10) 530 .width('90%') 531 .height(160) 532 } 533 .margin({ left: 10, right: 10 }) 534 .backgroundColor(0xFAEEE0) 535 } 536} 537 538@Reusable 539@Component 540struct ChildComponentB { 541 @State item: string = ''; 542 543 aboutToReuse(params: ESObject) { 544 this.item = params.item; 545 } 546 547 build() { 548 Row() { 549 Text(`Item ${this.item} Child Component B`) 550 .fontSize(20) 551 .margin({ left: 10 }) 552 .fontColor(Color.Red) 553 }.margin({ left: 10, right: 10 }) 554 } 555} 556 557@Reusable 558@Component 559struct ChildComponentC { 560 @State item: string = ''; 561 562 aboutToReuse(params: ESObject) { 563 this.item = params.item; 564 } 565 566 build() { 567 Row() { 568 Text(`Item ${this.item} Child Component C`) 569 .fontSize(20) 570 .margin({ left: 10 }) 571 .fontColor(Color.Green) 572 }.margin({ left: 10, right: 10 }) 573 } 574} 575 576@Reusable 577@Component 578struct ChildComponentD { 579 @State item: string = ''; 580 581 aboutToReuse(params: ESObject) { 582 this.item = params.item; 583 } 584 585 build() { 586 Row() { 587 Text(`Item ${this.item} Child Component D`) 588 .fontSize(20) 589 .margin({ left: 10 }) 590 .fontColor(Color.Orange) 591 }.margin({ left: 10, right: 10 }) 592 } 593} 594``` 595 596示例运行效果图如下: 597 598 599 600从效果图可以看出,每一个 ListItem 中的子组件 ChildComponentA 之间都可以触发组件复用。此时 ListItem 4 创建时,子组件 ChildComponentA 复用 ListItem 0 中的子组件 ChildComponentA ,复用仅耗时 864μs583ns。 601 602 603 604**应用场景案例** 605 606 607 608### 全局型 609 610 611 612一些场景中组件需要在不同的父组件中复用,并且不适合改为Builder。如上图所示,有时候应用在多个tab页之间切换,tab页之间结构类似,需要在tab页之间复用组件,提升页面切换性能。或者有些应用在组合型场景下,由于复用组件内部含有带状态的业务逻辑,不适合改为Builder函数。 613 614针对这种类型的组件复用场景,可以通过BuilderNode自定义缓存池,将要复用的组件封装在BuilderNode中,将BuilderNode的NodeController作为复用的最小单元,自行管理复用池。具体实现可以参考资料[全局自定义组件复用实现](./node_custom_component_reusable_pool.md)。 615 616这种场景不适用系统自带的复用池,自行管理组件复用。 617 618**应用场景案例** 619 620 621 622### 嵌套型 623 624 625 626复用组件的子组件的子组件之间存在差异。可以运行化归的思想,将复杂的问题转化为已知的、简单的问题。 627 628嵌套型实际上是上面四种类型的组件,以上图为例,可以通过有限变化型的方案,将子组件B变为子组件B1/B2/B3,这样问题就变成了一个标准的有限变化型。或者通过组合型的方案,将子组件B改为Builder,也可以将问题转化为一个标准有限变化型或者组合型的问题。 629 630### 无法复用型 631 632组件之间差别很大,规律性不强,子组件也不相同的组件之间进行复用。复用的含义就是重复使用相同布局的组件,布局完全不同的情况下,不建议使用组件复用。