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