• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 合理选择条件渲染和显隐控制
2
3开发者可以通过条件渲染或显隐控制两种方式来实现组件在显示和隐藏间的切换。本文从两者原理机制的区别出发,对二者适用场景分别进行说明,实现相应适用场景的示例并给出性能对比数据。
4
5## 原理机制
6
7### 条件渲染
8
9if/else条件渲染是ArkUI应用开发框架提供的渲染控制的能力之一。条件渲染可根据应用的不同状态,渲染对应分支下的UI描述。条件渲染的作用机制如下:
10
11- 页面初始构建时,会评估条件语句,构建适用分支的组件,若缺少适用分支,则不构建任何内容。
12- 应用状态变化时,会重新评估条件语句,删除不适用分支的组件,构建适用分支的组件,若缺少适用分支,则不构建任何内容。
13
14关于条件渲染的详细说明,可以参考[if/else:条件渲染](../quick-start/arkts-rendering-control-ifelse.md)。
15
16### 显隐控制
17
18显隐控制visibility是ArkUI应用开发框架提供的组件通用属性之一。开发者可以通过设定组件属性visibility不同的属性值,进而控制组件的显隐状态。visibility属性值及其描述如下:
19
20| 名称    | 描述                                     |
21| ------- | ---------------------------------------- |
22| Visible | 组件状态为可见                           |
23| Hidden  | 组件状态为不可见,但参与布局、进行占位   |
24| None    | 组件状态为不可见,不参与布局、不进行占位 |
25
26关于显隐控制的详细说明,可以参考[显隐控制](../reference/arkui-ts/ts-universal-attributes-visibility.md)。
27
28### 机制区别
29
30具体针对实现组件显示和隐藏间切换的场景,条件渲染和显隐控制的作用机制区别总结如下:
31
32| 机制描述                                               | 条件渲染 | 显隐控制 |
33| ------------------------------------------------------ | -------- | -------- |
34| 页面初始构建时,若组件隐藏,组件是否会被创建           | 否       | 是       |
35| 若组件由显示变为隐藏时,组件是否会被销毁、从组件树取下 | 是       | 否       |
36| 若组件隐藏时,是否占位                                 | 否       | 可以配置 |
37
38## 适用场景
39
40通过条件渲染或显隐控制,实现组件的显示和隐藏间的切换,两者的适用场景分别如下:
41
42条件渲染的适用场景:
43
44- 在应用冷启动阶段,应用加载绘制首页时,如果组件初始不需要显示,建议使用条件渲染替代显隐控制,以减少渲染时间,加快启动速度。
45- 如果组件不会较频繁地在显示和隐藏间切换,或者大部分时间不需要显示,建议使用条件渲染替代显隐控制,以减少界面复杂度、减少嵌套层次,提升性能。
46- 如果被控制的组件所占内存庞大,开发者优先考虑内存时,建议使用条件渲染替代显隐控制,以即时销毁不需要显示的组件,节省内存。
47- 如果组件子树结构比较复杂,且反复切换条件渲染的控制分支,建议使用条件渲染配合组件复用机制,提升应用性能。
48- 如果切换项仅涉及部分组件的情况,且反复切换条件渲染的控制分支,建议使用条件渲染配合容器限制,精准控制组件更新的范围,提升应用性能。
49
50显隐控制的适用场景:
51
52- 如果组件频繁地在显示和隐藏间切换时,建议使用显隐控制替代条件渲染,以避免组件的频繁创建与销毁,提升性能。
53- 如果组件隐藏后,在页面布局中,需要保持占位,建议适用显隐控制。
54
55### 显隐控制
56
57针对显示和隐藏间频繁切换的场景,下面示例通过按钮点击,实现1000张图片显示与隐藏,来简单复现该场景,并进行正反例性能数据的对比。
58
59**反例**
60
61使用条件循环实现显示和隐藏间的切换。
62
63```ts
64@Entry
65@Component
66struct WorseUseIf {
67  @State isVisible: boolean = true;
68  private data: number[] = [];
69
70  aboutToAppear() {
71    for (let i: number = 0; i < 1000; i++) {
72      this.data.push(i);
73    }
74  }
75
76  build() {
77    Column() {
78      Button("Switch visible and hidden").onClick(() => {
79        this.isVisible = !(this.isVisible);
80      }).width('100%')
81      Stack() {
82        if (this.isVisible) {// 使用条件渲染切换,会频繁创建与销毁组件
83          Scroll() {
84            Column() {
85              ForEach(this.data, (item: number) => {
86                Image($r('app.media.icon')).width('25%').height('12.5%')
87              }, (item: number) => item.toString())
88            }
89          }
90        }
91      }
92    }
93  }
94}
95```
96
97**正例**
98
99使用显隐控制实现显示和隐藏间的切换。
100
101```ts
102@Entry
103@Component
104struct BetterUseVisibility {
105  @State isVisible: boolean = true;
106  private data: number[] = [];
107
108  aboutToAppear() {
109    for (let i: number = 0; i < 1000; i++) {
110      this.data.push(i);
111    }
112  }
113
114  build() {
115    Column() {
116      Button("Switch visible and hidden").onClick(() => {
117        this.isVisible = !(this.isVisible);
118      }).width('100%')
119      Stack() {
120        Scroll() {
121          Column() {
122            ForEach(this.data, (item: number) => {
123              Image($r('app.media.icon')).width('25%').height('12.5%')
124            }, (item: number) => item.toString())
125          }
126        }.visibility(this.isVisible ? Visibility.Visible : Visibility.None)// 使用显隐控制切换,不会频繁创建与销毁组件
127      }
128    }
129  }
130}
131```
132
133**效果对比**
134
135正反例相同的操作步骤:通过点击按钮,将初始状态为显示的循环渲染组件切换为隐藏状态,再次点击按钮,将隐藏状态切换为显示状态。两次切换间的时间间隔长度,需保证页面渲染完成。
136
137此时组件从显示切换到隐藏状态,由于条件渲染会触发一次销毁组件,再从隐藏切换到显示,二次触发创建组件,此时用条件渲染实现切换的方式, 核心函数forEach耗时1s。
138
139![img](./figures/WorseUseIf.png)
140
141基于上例,由于显隐控制会将组件缓存到组件树,从缓存中取状态值修改,再从隐藏切换到显示,继续从缓存中取状态值修改,没有触发创建销毁组件,此时用显隐控制实现切换的方式,核心函数forEach耗时2ms。
142
143![img](./figures/BetterUseVisibility.png)
144
145可见,如果组件频繁地在显示和隐藏间切换时,使用显隐控制替代条件渲染,避免组件的频繁创建与销毁,可以提高性能。
146
147### 条件渲染
148
149针对应用冷启动,加载绘制首页时,如果组件初始不需要显示的场景,下面示例通过初始时,隐藏1000个Text组件,来简单复现该场景,并进行正反例性能数据的对比。
150
151**反例**
152
153对于首页初始时,不需要显示的组件,通过显隐控制进行隐藏。
154
155```ts
156@Entry
157@Component
158struct WorseUseVisibility {
159  @State isVisible: boolean = false; // 启动时,组件是隐藏状态
160  private data: number[] = [];
161
162  aboutToAppear() {
163    for (let i: number = 0; i < 1000; i++) {
164      this.data.push(i);
165    }
166  }
167
168  build() {
169    Column() {
170      Button("Show the Hidden on start").onClick(() => {
171        this.isVisible = !(this.isVisible);
172      }).width('100%')
173      Stack() {
174        Image($r('app.media.icon')).objectFit(ImageFit.Contain).width('50%').height('50%')
175        Scroll() {
176          Column() {
177            ForEach(this.data, (item: number) => {
178              Text(`Item value: ${item}`).fontSize(20).width('100%').textAlign(TextAlign.Center)
179            }, (item: number) => item.toString())
180          }
181        }.visibility(this.isVisible ? Visibility.Visible : Visibility.None)// 使用显隐控制,启动时即使组件处于隐藏状态,也会创建
182      }
183    }
184  }
185}
186```
187
188**正例**
189
190对于首页初始时,不需要显示的组件,通过条件渲染进行隐藏。
191
192```ts
193@Entry
194@Component
195struct BetterUseIf {
196  @State isVisible: boolean = false; // 启动时,组件是隐藏状态
197  private data: number[] = [];
198
199  aboutToAppear() {
200    for (let i: number = 0; i < 1000; i++) {
201      this.data.push(i);
202    }
203  }
204
205  build() {
206    Column() {
207      Button("Show the Hidden on start").onClick(() => {
208        this.isVisible = !(this.isVisible);
209      }).width('100%')
210      Stack() {
211        Image($r('app.media.icon')).objectFit(ImageFit.Contain).width('50%').height('50%')
212        if (this.isVisible) { // 使用条件渲染,启动时组件处于隐藏状态,不会创建
213          Scroll() {
214            Column() {
215              ForEach(this.data, (item: number) => {
216                Text(`Item value: ${item}`).fontSize(20).width('100%').textAlign(TextAlign.Center)
217              }, (item: number) => item.toString())
218            }
219          }
220        }
221      }
222    }
223  }
224}
225```
226
227**效果对比**
228
229正反例相同的操作步骤:通过hdc命令方式,采集应用主线程冷启动的CPU Profiler数据。具体操作,可以参考[应用性能分析工具CPU Profiler的使用指导](./application-performance-analysis.md#hdc-shell命令采集)。
230
231当应用加载绘制首页,大量组件初始不需要显示的冷启动场景时,如果组件初始不需要显示,此时使用显隐控制,启动时即使组件为隐藏状态也会创建组件。在UIAbility 启动阶段,以下为使用显隐控制的方式,渲染初始页面initialRenderView耗时401.1ms。
232
233![img](./figures/WorseUseVisibility.png)
234
235基于上例,如果组件初始不需要显示,此时使用条件渲染由于不满足渲染条件,启动时组件不会创建。在UIAbility 启动阶段,以下为使用条件渲染的方式,渲染初始页面initialRenderView耗时12.6ms。
236
237![img](./figures/BetterUseIf.png)
238
239可见,如果在应用冷启动阶段,应用加载绘制首页时,如果组件初始不需要显示,使用条件渲染替代显隐控制,可以减少渲染时间,加快启动速度。
240
241### 条件渲染和容器限制
242
243针对反复切换条件渲染的控制分支,但切换项仅涉及页面中少部分组件的场景,下面示例通过Column父组件下1000个Text组件,与1个受条件渲染控制的Text组件的组合来说明该场景,并对1个受条件渲染控制的Text组件的外面是否加上容器组件做包裹,做两种情况的正反例性能数据的对比。
244
245**反例**
246
247没有使用容器限制条件渲染组件的刷新范围,导致条件变化会触发创建和销毁该组件,影响该容器内所有组件都会刷新。
248
249```ts
250@Entry
251@Component
252struct RenderControlWithoutStack {
253  @State isVisible: boolean = true;
254  private data: number[] = [];
255
256  aboutToAppear() {
257    for (let i: number = 0; i < 1000; i++) {
258      this.data.push(i);
259    }
260  }
261
262  build() {
263    Column() {
264      Stack() {
265        Scroll() {
266          Column() { // 刷新范围会扩展到这一层
267            if (this.isVisible) { // 条件变化会触发创建和销毁该组件,影响到容器的布局,该容器内所有组件都会刷新
268              Text('New item').fontSize(20)
269            }
270            ForEach(this.data, (item: number) => {
271              Text(`Item value: ${item}`).fontSize(20).width('100%').textAlign(TextAlign.Center)
272            }, (item: number) => item.toString())
273          }
274        }
275      }.height('90%')
276
277      Button('Switch Hidden and Show').onClick(() => {
278        this.isVisible = !(this.isVisible);
279      })
280    }
281  }
282}
283```
284
285**正例**
286
287使用容器限制条件渲染组件的刷新范围。
288
289```ts
290@Entry
291@Component
292struct RenderControlWithStack {
293  @State isVisible: boolean = true;
294  private data: number[] = [];
295
296  aboutToAppear() {
297    for (let i: number = 0; i < 1000; i++) {
298      this.data.push(i);
299    }
300  }
301
302  build() {
303    Column() {
304      Stack() {
305        Scroll() {
306          Column() {
307            Stack() { // 在条件渲染外套一层容器,限制刷新范围
308              if (this.isVisible) {
309                Text('New item').fontSize(20)
310              }
311            }
312
313            ForEach(this.data, (item: number) => {
314              Text(`Item value: ${item}`).fontSize(20).width('100%').textAlign(TextAlign.Center)
315            }, (item: number) => item.toString())
316          }
317        }
318      }.height('90%')
319
320      Button('Switch Hidden and Show').onClick(() => {
321        this.isVisible = !(this.isVisible);
322      })
323    }
324  }
325}
326```
327
328**效果对比**
329
330正反例相同的操作步骤:通过点击按钮,将初始状态为显示的Text组件切换为隐藏状态,再次点击按钮,将隐藏状态切换为显示状态。两次切换间的时间间隔长度,需保证页面渲染完成。
331
332容器内有Text组件被if条件包含,if条件结果变更会触发创建和销毁该组件,此时影响到父组件Column容器的布局,该容器内所有组件都会刷新,包括模块ForEach,因此导致主线程UI刷新耗时过长。
333
334以下为未使用容器限制条件渲染组件的刷新范围的方式,Column组件被标记脏区,ForEach耗时13ms。
335
336![img](./figures/RenderControlWithoutStack.png)
337
338基于上例,容器内有Text组件被if条件包含,if条件结果变更会触发创建和销毁该组件,此时对于这种受状态变量控制的组件,在if外套一层Stack容器,只局部刷新if条件包含的组件。因此减少了主线程UI刷新耗时。
339
340以下为使用容器限制条件渲染组件的刷新范围的方式,Column组件没有被标记脏区,没有ForEach耗时。
341
342![img](./figures/RenderControlWithStack.png)
343
344可见,如果切换项仅涉及部分组件的情况,且反复切换条件渲染的控制分支,使用条件渲染配合容器限制,精准控制组件更新的范围,可以提升应用性能。
345
346### 条件渲染和组件复用
347
348针对反复切换条件渲染的控制分支,且控制分支中的每种分支内,组件子树结构都比较复杂的场景,当有可以复用的组件情况时,可以用组件复用配合条件渲染的方式提升性能。下面示例通过定义一个自定义复杂子组件MockComplexSubBranch配合条件渲染,来展示两种场景的性能效果对比,并对该组件复用与否做正反例性能数据的对比。
349
350**反例**
351
352没有使用组件复用实现条件渲染控制分支中的复杂子组件。
353
354```ts
355@Entry
356@Component
357struct IfWithoutReusable {
358  @State isAlignStyleStart: boolean = true;
359
360  build() {
361    Column() {
362      Button("Change FlexAlign").onClick(() => {
363        this.isAlignStyleStart = !this.isAlignStyleStart;
364      })
365      Stack() {
366        if (this.isAlignStyleStart) {
367          MockComplexSubBranch({ alignStyle: FlexAlign.Start }); // 未使用组件复用机制实现的MockComplexSubBranch
368        } else {
369          MockComplexSubBranch({ alignStyle: FlexAlign.End });
370        }
371      }
372    }
373  }
374}
375```
376
377其中MockComplexSubBranch是由3个Flex容器组件分别弹性布局200个Text组件构造而成,用以模拟组件复杂的子树结构,代码如下:
378
379```ts
380@Component
381export struct MockComplexSubBranch {
382  @State alignStyle: FlexAlign = FlexAlign.Center;
383
384  build() {
385    Column() {
386      Column({ space: 5 }) {
387        Text('ComplexSubBranch not reusable').fontSize(9).fontColor(0xCCCCCC).width('90%')
388        AlignContentFlex({ alignStyle: this.alignStyle });
389        AlignContentFlex({ alignStyle: this.alignStyle });
390        AlignContentFlex({ alignStyle: this.alignStyle });
391      }
392    }
393  }
394}
395
396@Component
397struct AlignContentFlex {
398  @Link alignStyle: FlexAlign;
399  private data: number[] = [];
400
401  aboutToAppear() {
402    for (let i: number = 0; i < 200; i++) {
403      this.data.push(i);
404    }
405  }
406
407  build() {
408    Flex({ wrap: FlexWrap.Wrap, alignContent: this.alignStyle }) {
409      ForEach(this.data, (item: number) => {
410        Text(`${item % 10}`).width('5%').height(20).backgroundColor(item % 2 === 0 ? 0xF5DEB3 : 0xD2B48C)
411      }, (item: number) => item.toString())
412    }.size({ width: '100%', height: 240 }).padding(10).backgroundColor(0xAFEEEE)
413  }
414}
415```
416
417**正例**
418
419使用组件复用实现条件渲染控制分支中的复杂子组件。
420
421```ts
422@Entry
423@Component
424struct IfWithReusable {
425  @State isAlignStyleStart: boolean = true;
426
427  build() {
428    Column() {
429      Button("Change FlexAlign").onClick(() => {
430        this.isAlignStyleStart = !this.isAlignStyleStart;
431      })
432      Stack() {
433        if (this.isAlignStyleStart) {
434          MockComplexSubBranch({ alignStyle: FlexAlign.Start }); // 使用组件复用机制实现的MockComplexSubBranch
435        } else {
436          MockComplexSubBranch({ alignStyle: FlexAlign.End });
437        }
438      }
439    }
440  }
441}
442```
443
444其中MockComplexSubBranch实现如下方所示,AlignContentFlex 代码一致,此处不再赘述。
445
446```ts
447@Component
448@Reusable // 添加Reusable装饰器,声明组件具备可复用的能力
449export struct MockComplexSubBranch {
450  @State alignStyle: FlexAlign = FlexAlign.Center;
451
452  aboutToReuse(params: ESObject) { // 从缓存复用组件前,更新组件的状态变量
453    this.alignStyle = params.alignStyle;
454  }
455
456  build() {
457    Column() {
458      Column({ space: 5 }) {
459        Text('ComplexSubBranch reusable').fontSize(9).fontColor(0xCCCCCC).width('90%')
460        AlignContentFlex({ alignStyle: this.alignStyle });
461        AlignContentFlex({ alignStyle: this.alignStyle });
462        AlignContentFlex({ alignStyle: this.alignStyle });
463      }
464    }
465  }
466}
467
468```
469
470**效果对比**
471
472正反例相同的操作步骤:通过点击按钮,Text组件会在Flex容器主轴上,由首端对齐转换为尾端对齐,再次点击按钮,由尾端对齐转换为首端对齐。两次切换间的时间间隔长度,需保证页面渲染完成。
473
474此时由于按钮反复切换了条件渲染分支,且每一分支中的MockComplexSubBranch组件子树结构都比较复杂,会造成大量的组件销毁创建过程,以下为不使用组件复用实现条件渲染控制分支中的子组件的方式,应用Index主页面渲染耗时180ms。
475
476![img](./figures/IfWithoutReusable.png)
477
478基于上例,考虑到将控制分支中的复杂组件子树结构在父组件中进行组件复用,此时从组件树缓存中拿出子组件,避免大量的组件销毁创建过程,以下为使用组件复用实现条件渲染控制分支中的子组件的方式,应用Index主页面渲染耗时14ms。
479
480![img](./figures/IfWithReusable.png)
481
482可见,针对反复切换条件渲染的控制分支的情况,且控制分支中的组件子树结构比较复杂,使用组件复用机制,可以提升应用性能。