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 140 141基于上例,由于显隐控制会将组件缓存到组件树,从缓存中取状态值修改,再从隐藏切换到显示,继续从缓存中取状态值修改,没有触发创建销毁组件,此时用显隐控制实现切换的方式,核心函数forEach耗时2ms。 142 143 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 234 235基于上例,如果组件初始不需要显示,此时使用条件渲染由于不满足渲染条件,启动时组件不会创建。在UIAbility 启动阶段,以下为使用条件渲染的方式,渲染初始页面initialRenderView耗时12.6ms。 236 237 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 337 338基于上例,容器内有Text组件被if条件包含,if条件结果变更会触发创建和销毁该组件,此时对于这种受状态变量控制的组件,在if外套一层Stack容器,只局部刷新if条件包含的组件。因此减少了主线程UI刷新耗时。 339 340以下为使用容器限制条件渲染组件的刷新范围的方式,Column组件没有被标记脏区,没有ForEach耗时。 341 342 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 477 478基于上例,考虑到将控制分支中的复杂组件子树结构在父组件中进行组件复用,此时从组件树缓存中拿出子组件,避免大量的组件销毁创建过程,以下为使用组件复用实现条件渲染控制分支中的子组件的方式,应用Index主页面渲染耗时14ms。 479 480 481 482可见,针对反复切换条件渲染的控制分支的情况,且控制分支中的组件子树结构比较复杂,使用组件复用机制,可以提升应用性能。