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/apis-arkui/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针对显示和隐藏间频繁切换的场景,下面示例通过按钮点击,实现1000张图片显示与隐藏,来简单复现该场景,并进行正反例性能数据的对比。 57 58**反例** 59 60使用条件循环实现显示和隐藏间的切换。 61 62```ts 63@Entry 64@Component 65struct WorseUseIf { 66 @State isVisible: boolean = true; 67 private data: number[] = []; 68 69 aboutToAppear() { 70 for (let i: number = 0; i < 1000; i++) { 71 this.data.push(i); 72 } 73 } 74 75 build() { 76 Column() { 77 Button("Switch visible and hidden").onClick(() => { 78 this.isVisible = !this.isVisible; 79 }).width('100%') 80 Stack() { 81 if (this.isVisible) {// 使用条件渲染切换,会频繁创建与销毁组件 82 Scroll() { 83 Column() { 84 ForEach(this.data, (item: number) => { 85 Image($r('app.media.icon')).width('25%').height('12.5%') 86 }, (item: number) => item.toString()) 87 } 88 } 89 } 90 } 91 } 92 } 93} 94``` 95 96**正例** 97 98使用显隐控制实现显示和隐藏间的切换。 99 100```ts 101@Entry 102@Component 103struct BetterUseVisibility { 104 @State isVisible: boolean = true; 105 private data: number[] = []; 106 107 aboutToAppear() { 108 for (let i: number = 0; i < 1000; i++) { 109 this.data.push(i); 110 } 111 } 112 113 build() { 114 Column() { 115 Button("Switch visible and hidden").onClick(() => { 116 this.isVisible = !this.isVisible; 117 }).width('100%') 118 Stack() { 119 Scroll() { 120 Column() { 121 ForEach(this.data, (item: number) => { 122 Image($r('app.media.icon')).width('25%').height('12.5%') 123 }, (item: number) => item.toString()) 124 } 125 }.visibility(this.isVisible ? Visibility.Visible : Visibility.None)// 使用显隐控制切换,不会频繁创建与销毁组件 126 } 127 } 128 } 129} 130``` 131 132**效果对比** 133 134正反例相同的操作步骤:通过点击按钮,将初始状态为显示的循环渲染组件切换为隐藏状态,再次点击按钮,将隐藏状态切换为显示状态。两次切换间的时间间隔长度,需保证页面渲染完成。 135 136此时组件从显示切换到隐藏状态,由于条件渲染会触发一次销毁组件,再从隐藏切换到显示,二次触发创建组件,此时用条件渲染实现切换的方式, 核心函数forEach耗时1s。 137 138 139 140基于上例,由于显隐控制会将组件缓存到组件树,从缓存中取状态值修改,再从隐藏切换到显示,继续从缓存中取状态值修改,没有触发创建销毁组件,此时用显隐控制实现切换的方式,核心函数forEach耗时2ms。 141 142 143 144可见,如果组件频繁地在显示和隐藏间切换时,使用显隐控制替代条件渲染,避免组件的频繁创建与销毁,可以提高性能。 145 146### 条件渲染 147 148针对应用冷启动,加载绘制首页时,如果组件初始不需要显示的场景,下面示例通过初始时,隐藏1000个Text组件,来简单复现该场景,并进行正反例性能数据的对比。 149 150**反例** 151 152对于首页初始时,不需要显示的组件,通过显隐控制进行隐藏。 153 154```ts 155@Entry 156@Component 157struct WorseUseVisibility { 158 @State isVisible: boolean = false; // 启动时,组件是隐藏状态 159 private data: number[] = []; 160 161 aboutToAppear() { 162 for (let i: number = 0; i < 1000; i++) { 163 this.data.push(i); 164 } 165 } 166 167 build() { 168 Column() { 169 Button("Show the Hidden on start").onClick(() => { 170 this.isVisible = !this.isVisible; 171 }).width('100%') 172 Stack() { 173 Image($r('app.media.icon')).objectFit(ImageFit.Contain).width('50%').height('50%') 174 Scroll() { 175 Column() { 176 ForEach(this.data, (item: number) => { 177 Text(`Item value: ${item}`).fontSize(20).width('100%').textAlign(TextAlign.Center) 178 }, (item: number) => item.toString()) 179 } 180 }.visibility(this.isVisible ? Visibility.Visible : Visibility.None)// 使用显隐控制,启动时即使组件处于隐藏状态,也会创建 181 } 182 } 183 } 184} 185``` 186 187**正例** 188 189对于首页初始时,不需要显示的组件,通过条件渲染进行隐藏。 190 191```ts 192@Entry 193@Component 194struct BetterUseIf { 195 @State isVisible: boolean = false; // 启动时,组件是隐藏状态 196 private data: number[] = []; 197 198 aboutToAppear() { 199 for (let i: number = 0; i < 1000; i++) { 200 this.data.push(i); 201 } 202 } 203 204 build() { 205 Column() { 206 Button("Show the Hidden on start").onClick(() => { 207 this.isVisible = !this.isVisible; 208 }).width('100%') 209 Stack() { 210 Image($r('app.media.icon')).objectFit(ImageFit.Contain).width('50%').height('50%') 211 if (this.isVisible) { // 使用条件渲染,启动时组件处于隐藏状态,不会创建 212 Scroll() { 213 Column() { 214 ForEach(this.data, (item: number) => { 215 Text(`Item value: ${item}`).fontSize(20).width('100%').textAlign(TextAlign.Center) 216 }, (item: number) => item.toString()) 217 } 218 } 219 } 220 } 221 } 222 } 223} 224``` 225 226**效果对比** 227 228正反例相同的操作步骤:通过hdc命令方式,采集应用主线程冷启动的CPU Profiler数据。具体操作,可以参考[应用性能分析工具CPU Profiler的使用指导](./application-performance-analysis.md#hdc-shell命令采集)。 229 230当应用加载绘制首页,大量组件初始不需要显示的冷启动场景时,如果组件初始不需要显示,此时使用显隐控制,启动时即使组件为隐藏状态也会创建组件。在UIAbility 启动阶段,以下为使用显隐控制的方式,渲染初始页面initialRenderView耗时401.1ms。 231 232 233 234基于上例,如果组件初始不需要显示,此时使用条件渲染由于不满足渲染条件,启动时组件不会创建。在UIAbility 启动阶段,以下为使用条件渲染的方式,渲染初始页面initialRenderView耗时12.6ms。 235 236 237 238可见,如果在应用冷启动阶段,应用加载绘制首页时,如果组件初始不需要显示,使用条件渲染替代显隐控制,可以减少渲染时间,加快启动速度。 239 240**效果对比** 241 242正反例相同的操作步骤:通过点击按钮,将初始状态为显示的Text组件切换为隐藏状态,再次点击按钮,将隐藏状态切换为显示状态。两次切换间的时间间隔长度,需保证页面渲染完成。 243 244容器内有Text组件被if条件包含,if条件结果变更会触发创建和销毁该组件,此时影响到父组件Column容器的布局,该容器内所有组件都会刷新,包括模块ForEach,因此导致主线程UI刷新耗时过长。 245 246以下为未使用容器限制条件渲染组件的刷新范围的方式,Column组件被标记脏区,ForEach耗时13ms。 247 248 249 250基于上例,容器内有Text组件被if条件包含,if条件结果变更会触发创建和销毁该组件,此时对于这种受状态变量控制的组件,在if外套一层Stack容器,只局部刷新if条件包含的组件。因此减少了主线程UI刷新耗时。 251 252以下为使用容器限制条件渲染组件的刷新范围的方式,Column组件没有被标记脏区,没有ForEach耗时。 253 254 255 256可见,如果切换项仅涉及部分组件的情况,且反复切换条件渲染的控制分支,使用条件渲染配合容器限制,精准控制组件更新的范围,可以提升应用性能。 257 258### 条件渲染和组件复用 259 260针对反复切换条件渲染的控制分支,且控制分支中的每种分支内,组件子树结构都比较复杂的场景,当有可以复用的组件情况时,可以用组件复用配合条件渲染的方式提升性能。下面示例通过定义一个自定义复杂子组件MockComplexSubBranch配合条件渲染,来展示两种场景的性能效果对比,并对该组件复用与否做正反例性能数据的对比。 261 262**反例** 263 264没有使用组件复用实现条件渲染控制分支中的复杂子组件。 265 266```ts 267@Entry 268@Component 269struct IfWithoutReusable { 270 @State isAlignStyleStart: boolean = true; 271 272 build() { 273 Column() { 274 Button("Change FlexAlign").onClick(() => { 275 this.isAlignStyleStart = !this.isAlignStyleStart; 276 }) 277 Stack() { 278 if (this.isAlignStyleStart) { 279 MockComplexSubBranch({ alignStyle: FlexAlign.Start }); // 未使用组件复用机制实现的MockComplexSubBranch 280 } else { 281 MockComplexSubBranch({ alignStyle: FlexAlign.End }); 282 } 283 } 284 } 285 } 286} 287``` 288 289其中MockComplexSubBranch是由3个Flex容器组件分别弹性布局200个Text组件构造而成,用以模拟组件复杂的子树结构,代码如下: 290 291```ts 292@Component 293export struct MockComplexSubBranch { 294 @State alignStyle: FlexAlign = FlexAlign.Center; 295 296 build() { 297 Column() { 298 Column({ space: 5 }) { 299 Text('ComplexSubBranch not reusable').fontSize(9).fontColor(0xCCCCCC).width('90%') 300 AlignContentFlex({ alignStyle: this.alignStyle }); 301 AlignContentFlex({ alignStyle: this.alignStyle }); 302 AlignContentFlex({ alignStyle: this.alignStyle }); 303 } 304 } 305 } 306} 307 308@Component 309struct AlignContentFlex { 310 @Link alignStyle: FlexAlign; 311 private data: number[] = []; 312 313 aboutToAppear() { 314 for (let i: number = 0; i < 200; i++) { 315 this.data.push(i); 316 } 317 } 318 319 build() { 320 Flex({ wrap: FlexWrap.Wrap, alignContent: this.alignStyle }) { 321 ForEach(this.data, (item: number) => { 322 Text(`${item % 10}`).width('5%').height(20).backgroundColor(item % 2 === 0 ? 0xF5DEB3 : 0xD2B48C) 323 }, (item: number) => item.toString()) 324 }.size({ width: '100%', height: 240 }).padding(10).backgroundColor(0xAFEEEE) 325 } 326} 327``` 328 329**正例** 330 331使用组件复用实现条件渲染控制分支中的复杂子组件。 332 333```ts 334@Entry 335@Component 336struct IfWithReusable { 337 @State isAlignStyleStart: boolean = true; 338 339 build() { 340 Column() { 341 Button("Change FlexAlign").onClick(() => { 342 this.isAlignStyleStart = !this.isAlignStyleStart; 343 }) 344 Stack() { 345 if (this.isAlignStyleStart) { 346 MockComplexSubBranch({ alignStyle: FlexAlign.Start }); // 使用组件复用机制实现的MockComplexSubBranch 347 } else { 348 MockComplexSubBranch({ alignStyle: FlexAlign.End }); 349 } 350 } 351 } 352 } 353} 354``` 355 356其中MockComplexSubBranch实现如下方所示,AlignContentFlex 代码一致,此处不再赘述。 357 358```ts 359@Component 360@Reusable // 添加Reusable装饰器,声明组件具备可复用的能力 361export struct MockComplexSubBranch { 362 @State alignStyle: FlexAlign = FlexAlign.Center; 363 364 aboutToReuse(params: ESObject) { // 从缓存复用组件前,更新组件的状态变量 365 this.alignStyle = params.alignStyle; 366 } 367 368 build() { 369 Column() { 370 Column({ space: 5 }) { 371 Text('ComplexSubBranch reusable').fontSize(9).fontColor(0xCCCCCC).width('90%') 372 AlignContentFlex({ alignStyle: this.alignStyle }); 373 AlignContentFlex({ alignStyle: this.alignStyle }); 374 AlignContentFlex({ alignStyle: this.alignStyle }); 375 } 376 } 377 } 378} 379 380``` 381 382**效果对比** 383 384正反例相同的操作步骤:通过点击按钮,Text组件会在Flex容器主轴上,由首端对齐转换为尾端对齐,再次点击按钮,由尾端对齐转换为首端对齐。两次切换间的时间间隔长度,需保证页面渲染完成。 385 386此时由于按钮反复切换了条件渲染分支,且每一分支中的MockComplexSubBranch组件子树结构都比较复杂,会造成大量的组件销毁创建过程,以下为不使用组件复用实现条件渲染控制分支中的子组件的方式,应用Index主页面渲染耗时180ms。 387 388 389 390基于上例,考虑到将控制分支中的复杂组件子树结构在父组件中进行组件复用,此时从组件树缓存中拿出子组件,避免大量的组件销毁创建过程,以下为使用组件复用实现条件渲染控制分支中的子组件的方式,应用Index主页面渲染耗时14ms。 391 392 393 394可见,针对反复切换条件渲染的控制分支的情况,且控制分支中的组件子树结构比较复杂,使用组件复用机制,可以提升应用性能。