• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![normal](./figures/component_reuse_overview_normal.png)
34
35这是一个标准的组件复用场景,一个滚动容器内的复用组件布局相同,只有数据不同。这种类型的组件复用可以直接参考资料[组件复用实践](./component-recycle.md)。
36
37**应用场景案例**
38
39![normal_case](./figures/component_reuse_overview_normal_case.png)
40
41### 有限变化型
42
43![limited](./figures/component_reuse_overview_limited.png)
44
45这种类型中复用组件之间存在不同,但是类型有限。如上图所示,容器内的复用组件内部的子组件不一样,但可总结为两种类型,类型 1由三个子组件 A 进行布局拼接而成,类型 2由子组件 B、子组件 C 和子组件 D 进行布局拼接而成。
46
47此时存在以下两种应对措施:
48
49- **类型1和类型2业务逻辑不同**:建议将两种类型的组件使用两个不同的自定义组件,分别进行复用。此时组件复用池内的状态如下图所示,复用组件 1 和复用组件 2 处于不同的复用 list 中。
50
51![limited_first_method_cache](./figures/component_reuse_overview_limited_first_method_cache.png)
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![limited_second_method_cache](./figures/component_reuse_overview_limited_second_method_cache.png)
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![limited_case.png](./figures/component_reuse_overview_limited_case.png)
195
196### 组合型
197
198![composition](./figures/component_reuse_overview_composition.png)
199
200这种类型中复用组件之间存在不同,并且情况非常多,但拥有共同的子组件。如果使用有限变化型的组件复用方式,将所有类型的复用组件写成自定义组件分别复用,那么不同复用组件的复用 list 中相同的子组件之间不能互相复用。对此可以将复用组件转变为 Builder 函数,使复用组件内部共同的子组件的缓存池在父组件上共享。此时组件复用池内的状态如下图所示。
201
202![composition_cache](./figures/component_reuse_overview_composition_cache.png)
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![composition_optimization_before](./figures/component_reuse_overview_composition_optimization_before.gif)
418
419从上图可以看到,列表滑动到 ListItem 0 消失时,复用组件 ReusableComponentOne 和它的子组件 ChildComponentA 都加入了复用缓存。继续向上滑动时,由于 ListItem 4 与 ListItem 0 的复用组件不在同一个复用 list,因此 ListItem 4 的复用组件 ReusableComponentThree 和它的子组件依然会全部重新创建,不会复用缓存中的子组件 ChildComponentA。
420
421此时 ListItem 4 中的子组件 ChildComponentA 的重新创建耗时 6ms387μs499ns。
422
423![composition_optimization_before_trace](./figures/component_reuse_overview_composition_optimization_before.png)
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![composition_optimization_after](./figures/component_reuse_overview_composition_optimization_after.gif)
599
600从效果图可以看出,每一个 ListItem 中的子组件 ChildComponentA 之间都可以触发组件复用。此时 ListItem 4 创建时,子组件 ChildComponentA 复用 ListItem 0 中的子组件 ChildComponentA ,复用仅耗时 864μs583ns。
601
602![composition_optimization_after_trace](./figures/component_reuse_overview_composition_optimization_after.png)
603
604**应用场景案例**
605
606![composition_case.png](./figures/component_reuse_overview_composition_case.png)
607
608### 全局型
609
610![component_reuse_overview_global](./figures/component_reuse_overview_global.png)
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![global_tab_switching](./figures/component_reuse_overview_global_tab_switching.gif)
621
622### 嵌套型
623
624![component_reuse_overview_nested](./figures/component_reuse_overview_nested.png)
625
626复用组件的子组件的子组件之间存在差异。可以运行化归的思想,将复杂的问题转化为已知的、简单的问题。
627
628嵌套型实际上是上面四种类型的组件,以上图为例,可以通过有限变化型的方案,将子组件B变为子组件B1/B2/B3,这样问题就变成了一个标准的有限变化型。或者通过组合型的方案,将子组件B改为Builder,也可以将问题转化为一个标准有限变化型或者组合型的问题。
629
630### 无法复用型
631
632组件之间差别很大,规律性不强,子组件也不相同的组件之间进行复用。复用的含义就是重复使用相同布局的组件,布局完全不同的情况下,不建议使用组件复用。