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![normal](./figures/component_reuse_overview_normal.png)
27
28这是一个标准的组件复用场景,一个滚动容器内的复用组件布局相同,只有数据不同。这种类型的组件复用可以直接参考资料[组件复用实践](./component-recycle.md)。
29
30**应用场景案例**
31
32![normal_case](./figures/component_reuse_overview_normal_case.png)
33
34### 有限变化型
35
36![limited](./figures/component_reuse_overview_limited.png)
37
38这种类型中复用组件之间存在不同,但是类型有限。如上图所示,容器内的复用组件内部的子组件不一样,但可总结为两种类型,类型 1由三个子组件 A 进行布局拼接而成,类型 2由子组件 B、子组件 C 和子组件 D 进行布局拼接而成。
39
40此时存在以下两种应对措施:
41
42- **类型1和类型2业务逻辑不同**:建议将两种类型的组件使用两个不同的自定义组件,分别进行复用。此时组件复用池内的状态如下图所示,复用组件 1 和复用组件 2 处于不同的复用 list 中。
43
44![limited_first_method_cache](./figures/component_reuse_overview_limited_first_method_cache.png)
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![limited_second_method_cache](./figures/component_reuse_overview_limited_second_method_cache.png)
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![limited_case.png](./figures/component_reuse_overview_limited_case.png)
188
189### 组合型
190
191![composition](./figures/component_reuse_overview_composition.png)
192
193这种类型中复用组件之间存在不同,并且情况非常多,但拥有共同的子组件。如果使用有限变化型的组件复用方式,将所有类型的复用组件写成自定义组件分别复用,那么不同复用组件的复用 list 中相同的子组件之间不能互相复用。对此可以将复用组件转变为 Builder 函数,使复用组件内部共同的子组件的缓存池在父组件上共享。此时组件复用池内的状态如下图所示。
194
195![composition_cache](./figures/component_reuse_overview_composition_cache.png)
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![composition_optimization_before](./figures/component_reuse_overview_composition_optimization_before.gif)
411
412从上图可以看到,列表滑动到 ListItem 0 消失时,复用组件 ReusableComponentOne 和它的子组件 ChildComponentA 都加入了复用缓存。继续向上滑动时,由于 ListItem 4 与 ListItem 0 的复用组件不在同一个复用 list,因此 ListItem 4 的复用组件 ReusableComponentThree 和它的子组件依然会全部重新创建,不会复用缓存中的子组件 ChildComponentA。
413
414此时 ListItem 4 中的子组件 ChildComponentA 的重新创建耗时 6ms387μs499ns。
415
416![composition_optimization_before_trace](./figures/component_reuse_overview_composition_optimization_before.png)
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![composition_optimization_after](./figures/component_reuse_overview_composition_optimization_after.gif)
592
593从效果图可以看出,每一个 ListItem 中的子组件 ChildComponentA 之间都可以触发组件复用。此时 ListItem 4 创建时,子组件 ChildComponentA 复用 ListItem 0 中的子组件 ChildComponentA ,复用仅耗时 864μs583ns。
594
595![composition_optimization_after_trace](./figures/component_reuse_overview_composition_optimization_after.png)
596
597**应用场景案例**
598
599![composition_case.png](./figures/component_reuse_overview_composition_case.png)
600
601### 全局型
602
603![component_reuse_overview_global](./figures/component_reuse_overview_global.png)
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![global_tab_switching](./figures/component_reuse_overview_global_tab_switching.gif)
614
615### 嵌套型
616
617![component_reuse_overview_nested](./figures/component_reuse_overview_nested.png)
618
619复用组件的子组件的子组件之间存在差异。可以运行化归的思想,将复杂的问题转化为已知的、简单的问题。
620
621嵌套型实际上是上面四种类型的组件,以上图为例,可以通过有限变化型的方案,将子组件B变为子组件B1/B2/B3,这样问题就变成了一个标准的有限变化型。或者通过组合型的方案,将子组件B改为Builder,也可以将问题转化为一个标准有限变化型或者组合型的问题。
622
623### 无法复用型
624
625组件之间差别很大,规律性不强,子组件也不相同的组件之间进行复用。复用的含义就是重复使用相同布局的组件,布局完全不同的情况下,不建议使用组件复用。