1# 组件复用实践 2 3若开发者的应用中存在以下场景,并成为UI线程的帧率瓶颈,应该考虑使用组件复用机制提升应用性能: 4 51. 滑动场景下对同一类自定义组件的实例进行频繁的创建与销毁。 62. 反复切换条件渲染的控制分支,且控制分支中的组件子树结构比较复杂。 7 8组件复用生效的条件是: 9 101. 自定义组件被@Reusable装饰器修饰,即表示其具备组件复用的能力。 112. 在一个自定义组件(父)下创建出来的具备组件复用能力的自定义组件(子),在可复用自定义组件从组件树上移除之后,会被加入到其父自定义组件的可复用组件缓存中。 123. 在一个自定义组件(父)下创建可复用的子组件时,若可复用子节点缓存中有对应类型的可复用子组件的实例,会通过更新可复用子组件的方式,快速创建可复用子组件。 13 14## 约束限制 15 161. @Reusable标识自定义组件具备可复用的能力,它可以被添加到任意的自定义组件上,但是开发者需要小心处理自定义组件的**创建流程**和**更新流程**以确保自定义组件在复用之后能展示出正确的行为。 172. 可复用自定义组件的缓存和复用只能发生在同一父组件下,无法在不同的父组件下复用同一自定义组件的实例。例如,A组件是可复用组件,其也是B组件的子组件,并进入了B组件的可复用组件缓存中,但是在C组件中创建A组件时,无法使用B组件缓存的A组件。 183. @Reusable装饰器只需要对复用子树的根节点进行标记。例如:自定义组件A中有一个自定义子组件B,若需要复用A与B的子树,只需要对A组件添加@Reusable装饰器。 194. 可复用自定义组件中嵌套自定义组件,如果想要对嵌套的子组件的内容进行更新,需要实现对应子组件的aboutToReuse生命周期回调。例如:A组件是可复用的组件,B是A中嵌套的子组件,要想实现对A组件中的B组件内容进行更新,需要在B组件中实现aboutToReuse生命周期回调。 205. 自定义组件的复用带来的性能提升主要体现在节省了自定义组件的JS对象的创建时间并复用了自定义组件的组件树结构,若应用开发者在自定义组件复用的前后使用渲染控制语法显著的改变了自定义组件的组件树结构,那么将无法享受到组件复用带来的性能提升。 216. 组件复用仅发生在存在可复用组件从组件树上移除并再次加入到组件树的场景中,若不存在上述场景,将无法触发组件复用。例如,使用ForEach渲染控制语法创建可复用的自定义组件,由于ForEach渲染控制语法的全展开属性,不能触发组件复用。 227. 组件复用当前不支持嵌套使用。即在可复用的组件的子树中存在可复用的组件,可能导致未定义的结果。 23 24## 生命周期 25 26可复用组件从C++侧的组件树上移除时,自定义组件在ArkUI框架native侧的CustomNode会被挂载到其对应的JSView上。复用发生之后,CustomNode被JSView引用,并触发ViewPU上的aboutToRecycle方法,ViewPU的实例将会被RecycleManager引用。 27 28可复用组件从RecycleManager中重新加入组件树时,会调用前端ViewPU对象上的aboutToReuse生命周期回调。 29 30## 接口说明 31 32组件的生命周期回调,在可复用组件从复用缓存中加入到组件树之前调用,可在其中更新组件的状态变量以展示正确的内容,入参的类型与自定义组件的构造函数入参相同。 33 34```ts 35aboutToReuse?(params: { [key: string]: unknown }): void; 36``` 37 38组件的生命周期回调,在可复用组件从组件树上被加入到复用缓存之前调用。 39 40```ts 41aboutToRecycle?(): void; 42``` 43 44开发者可以使用reuseId为复用组件分配复用组,相同reuseId的组件会在同一个复用组中复用。 45 46```ts 47reuseId(id: string); 48``` 49 50Reusable装饰器,用于声明组件具备可复用的能力。 51 52```ts 53declare const Reusable: ClassDecorator; 54``` 55 56**示例:** 57 58```ts 59// xxx.ets 60class MyDataSource implements IDataSource { 61 private dataArray: string[] = []; 62 private listener: DataChangeListener | undefined; 63 64 public totalCount(): number { 65 return this.dataArray.length; 66 } 67 68 public getData(index: number): string { 69 return this.dataArray[index]; 70 } 71 72 public pushData(data: string): void { 73 this.dataArray.push(data); 74 } 75 76 public reloadListener(): void { 77 this.listener?.onDataReloaded(); 78 } 79 80 public registerDataChangeListener(listener: DataChangeListener): void { 81 this.listener = listener; 82 } 83 84 public unregisterDataChangeListener(listener: DataChangeListener): void { 85 this.listener = undefined; 86 } 87} 88 89@Entry 90@Component 91struct MyComponent { 92 private data: MyDataSource = new MyDataSource(); 93 94 aboutToAppear() { 95 for (let i = 0; i < 1000; i++) { 96 this.data.pushData(i.toString()) 97 } 98 } 99 100 build() { 101 List({ space: 3 }) { 102 LazyForEach(this.data, (item: string) => { 103 ListItem() { 104 ReusableChildComponent({ item: item }) 105 } 106 }, (item: string) => item) 107 } 108 .width('100%') 109 .height('100%') 110 } 111} 112 113@Reusable 114@Component 115struct ReusableChildComponent { 116 @State item: string = '' 117 118 aboutToReuse(params: ESObject) { 119 this.item = params.item; 120 } 121 122 build() { 123 Row() { 124 Text(this.item) 125 .fontSize(20) 126 .margin({ left: 10 }) 127 }.margin({ left: 10, right: 10 }) 128 } 129} 130``` 131 132## 相关实例 133 134以下为购物片段示例代码,对比使用组件复用前后,应用侧创建自定义组件的收益以及前后的代码写法对比。 135 136### 复用前后代码对比 137 138**复用前:** 139 140```ts 141LazyForEach(this.GoodDataOne, (item, index) => { 142 GridItem() { 143 Column() { 144 Image(item.img) 145 .height(item.hei) 146 .width('100%') 147 .objectFit(ImageFit.Fill) 148 149 Text(item.introduce) 150 .fontSize(14) 151 .padding({ left: 5, right: 5 }) 152 .margin({ top: 5 }) 153 Row() { 154 Row() { 155 Text('¥') 156 .fontSize(10) 157 .fontColor(Color.Red) 158 .baselineOffset(-4) 159 Text(item.price) 160 .fontSize(16) 161 .fontColor(Color.Red) 162 Text(item.numb) 163 .fontSize(10) 164 .fontColor(Color.Gray) 165 .baselineOffset(-4) 166 .margin({ left: 5 }) 167 } 168 169 Image($r('app.media.photo63')) 170 .width(20) 171 .height(10) 172 .margin({ bottom: -8 }) 173 } 174 .width('100%') 175 .justifyContent(FlexAlign.SpaceBetween) 176 .padding({ left: 5, right: 5 }) 177 .margin({ top: 15 }) 178 } 179 .borderRadius(10) 180 .backgroundColor(Color.White) 181 .clip(true) 182 .width('100%') 183 .height(290) 184 } 185}, (item) => JSON.stringify(item)) 186``` 187 188**复用后:** 189 190组件被复用后,ArkUI框架会将组件构造对应的参数输入给aboutToReuse生命周期回调,开发者需要在aboutToReuse生命周期中对需要进行更新的状态变量进行赋值,ArkUI框架将会基于最新的状态变量值对UI进行展示。 191 192如果同一种自定义组件的不同实例之间存在较大的结构差异,建议使用reuseId对不同的自定义组件实例分别标注复用组,以达到最佳的复用效果。 193 194如果一个自定义组件中,持有对某个大对象或者其他非必要资源的引用,可以在aboutToRecycle生命周期中释放,以免造成内存泄漏。 195 196```ts 197LazyForEach(this.GoodDataOne, (item, index) => { 198 GridItem() { 199 GoodItems({ 200 boo:item.data.boo, 201 img:item.data.img, 202 webimg:item.data.webimg, 203 hei:item.data.hei, 204 introduce:item.data.introduce, 205 price:item.data.price, 206 numb:item.data.numb, 207 index:index 208 }) 209 .reuseId(this.CombineStr(item.type)) 210 } 211}, (item) => JSON.stringify(item)) 212 213 214@Reusable 215@Component 216struct GoodItems { 217 @State img: Resource = $r("app.media.photo61") 218 @State webimg?: string = '' 219 @State hei: number = 0 220 @State introduce: string = '' 221 @State price: string = '' 222 @State numb: string = '' 223 @LocalStorageLink('storageSimpleProp') simpleVarName: string = '' 224 boo: boolean = true 225 index: number = 0 226 controllerVideo: VideoController = new VideoController(); 227 228 aboutToReuse(params) 229 { 230 this.webimg = params.webimg 231 this.img = params.img 232 this.hei = params.hei 233 this.introduce = params.introduce 234 this.price = params.price 235 this.numb = params.numb 236 } 237 238 build() { 239 // ... 240 } 241} 242``` 243 244### 性能收益 245 246通过DevEco Studio的profiler工具分析复用前后的组件创建时间,可以得到应用使能组件复用后的优化情况,组件创建的时间平均从1800us降低到了570us。 247 248 249 250 251 252| | 创建组件时间 | 253| -------------- | ------------ | 254| 不使能组件复用 | 1813us | 255| 使能组件复用 | 570us | 256 257## 开发建议 258 2591.建议复用自定义组件时避免一切可能改变自定义组件的组件树结构和可能使可复用组件中产生重新布局的操作以将组件复用的性能提升到最高。 260 2612.建议列表滑动场景下组件复用能力和LazyForEach渲染控制语法搭配使用以达到性能最优效果。 262 2633.开发者需要区分好自定义组件的创建和更新过程中的行为,并注意到自定义组件的复用本质上是一种特殊的组件更新行为,组件创建过程中的流程与生命周期将不会在组件复用中发生,自定义组件的构造参数将通过aboutToReuse生命周期回调传递给自定义组件。例如,aboutToAppear生命周期和自定义组件的初始化传参将不会在组件复用中发生。 264 2654.避免在aboutToReuse生命周期回调中产生耗时操作,最佳实践是仅在aboutToReuse中做自定义组件更新所需的状态变量值的更新。 266 2675.避免在aboutToReuse中对@Link、@StorageLink、@ObjectLink、@Consume等自动更新值的状态变量进行更新,可能触发不必要的组件刷新。 268 2696.避免使用函数作为复用的自定义组件创建时的入参: 270 271由于在组件复用的场景下,每次复用都需要重新创建组件关联的数据对象,导致重复执行入参中的函数来获取入参结果。如果函数中存在耗时操作,会严重影响性能。正反例如下所示: 272 273【反例】 274 275```ts 276// 下文中BasicDateSource是实现IDataSource接口的类,具体可参考LazyForEach用法指导 277// 此处为复用的自定义组件 278@Reusable 279@Component 280struct ChildComponent { 281 @State desc: string = ''; 282 @State sum: number = 0; 283 284 aboutToReuse(params: Record<string, Object>): void { 285 this.desc = params.desc as string; 286 this.sum = params.sum as number; 287 } 288 289 build() { 290 Column() { 291 Text('子组件' + this.desc) 292 .fontSize(30) 293 .fontWeight(30) 294 Text('结果' + this.sum) 295 .fontSize(30) 296 .fontWeight(30) 297 } 298 } 299} 300 301@Entry 302@Component 303struct Reuse { 304 private data: BasicDateSource = new BasicDateSource(); 305 306 aboutToAppear(): void { 307 for (let index = 0; index < 20; index++) { 308 this.data.pushData(index.toString()) 309 } 310 } 311 312 // 真实场景的函数中可能存在未知的耗时操作逻辑,此处用循环函数模拟耗时操作 313 count(): number { 314 let temp: number = 0; 315 for (let index = 0; index < 10000; index++) { 316 temp += index; 317 } 318 return temp; 319 } 320 321 build() { 322 Column() { 323 List() { 324 LazyForEach(this.data, (item: string) => { 325 ListItem() { 326 // 此处sum参数是函数获取的,实际开发场景无法预料该函数可能出现的耗时操作,每次进行组件复用都会重复触发此函数的调用 327 ChildComponent({ desc: item, sum: this.count() }) 328 } 329 .width('100%') 330 .height(100) 331 }, (item: string) => item) 332 } 333 } 334 } 335} 336``` 337 338上述反例操作中,复用的子组件参数sum是通过耗时函数生成。该函数在每次组件复用时都需要执行,会造成性能问题,甚至是列表滑动过程中的卡顿丢帧现象。 339 340【正例】 341 342```ts 343// 下文中BasicDateSource是实现IDataSource接口的类,具体可参考LazyForEach用法指导 344// 此处为复用的自定义组件 345@Reusable 346@Component 347struct ChildComponent { 348 @State desc: string = ''; 349 @State sum: number = 0; 350 351 aboutToReuse(params: Record<string, Object>): void { 352 this.desc = params.desc as string; 353 this.sum = params.sum as number; 354 } 355 356 build() { 357 Column() { 358 Text('子组件' + this.desc) 359 .fontSize(30) 360 .fontWeight(30) 361 Text('结果' + this.sum) 362 .fontSize(30) 363 .fontWeight(30) 364 } 365 } 366} 367 368@Entry 369@Component 370struct Reuse { 371 private data: BasicDateSource = new BasicDateSource(); 372 @State sum: number = 0; 373 374 aboutToAppear(): void { 375 for (let index = 0; index < 20; index++) { 376 this.data.pushData(index.toString()) 377 } 378 // 执行该异步函数 379 this.count(); 380 } 381 382 // 模拟耗时操作逻辑 383 async count() { 384 let temp: number = 0; 385 for (let index = 0; index < 10000; index++) { 386 temp += index; 387 } 388 // 将结果放入状态变量中 389 this.sum = temp; 390 } 391 392 build() { 393 Column() { 394 List() { 395 LazyForEach(this.data, (item: string) => { 396 ListItem() { 397 // 子组件的传参通过状态变量进行 398 ChildComponent({ desc: item, sum: this.sum }) 399 } 400 .width('100%') 401 .height(100) 402 }, (item: string) => item) 403 } 404 } 405 } 406} 407``` 408 409上述正例操作中,通过耗时函数count生成的结果不变,可以将其放到页面初始渲染时执行一次,将结果赋值给this.sum。在复用组件的参数传递时,通过this.sum来进行。 410