1# \@ReusableV2装饰器:组件复用 2 3为了降低反复创建销毁自定义组件带来的性能开销,开发者可以使用\@ReusableV2装饰\@ComponentV2装饰的自定义组件,达成组件复用的效果。 4 5在阅读本文前,建议提前阅读:[\@Reusable装饰器:组件复用](./arkts-reusable.md)。 6 7>**说明:** 8> 9>从API version 18开始,可以使用\@ReusableV2装饰\@ComponentV2装饰的自定义组件。 10> 11 12## 概述 13 14\@ReusableV2用于装饰V2的自定义组件,表明该自定义组件具有被复用的能力: 15 16- \@ReusableV2仅能装饰V2的自定义组件,即\@ComponentV2装饰的自定义组件。并且仅能将\@ReusableV2装饰的自定义组件作为V2自定义组件的子组件使用。 17- \@ReusableV2同样提供了aboutToRecycle和aboutToReuse的生命周期,在组件被回收时调用aboutToRecycle,在组件被复用时调用aboutToReuse,但与\@Reusable不同的是,aboutToReuse没有入参。 18- 在回收阶段,会递归地调用所有子组件的aboutToRecycle回调(即使子组件未被标记可复用);在复用阶段,会递归地调用所有子组件的aboutToReuse回调(即使子组件未被标记可复用)。 19- \@ReusableV2装饰的自定义组件会在被回收期间保持冻结状态,即无法触发UI刷新、无法触发\@Monitor回调,与freezeWhenInactive标记位不同的是,在解除冻结状态后,不会触发延后的刷新。 20- \@ReusableV2装饰的自定义组件会在复用时自动重置组件内状态变量的值、重新计算组件内\@Computed以及与之相关的\@Monitor。不建议开发者在aboutToRecycle中更改组件内状态变量,详见[复用前的组件内状态变量重置](#复用前的组件内状态变量重置)。 21- V1和V2的复用组件可在一定规则下混用,详见[使用限制](#使用限制)第二点。 22- 不建议开发者嵌套滥用\@ReusableV2装饰器,这可能会导致复用效率降低以及内存开销变大。 23 24## 装饰器说明 25 26| \@ReusableV2装饰器 | 说明 | 27| ------------------ | ----------------------------- | 28| 装饰器参数 | 无 | 29| 可装饰的组件 | \@ComponentV2装饰的自定义组件 | 30| 装饰器作用 | 表明该组件可被复用 | 31 32```ts 33@ReusableV2 // 装饰ComponentV2的自定义组件 34@ComponentV2 35struct ReusableV2Component { 36 @Local message: string = 'Hello World'; 37 build () { 38 Column() { 39 Text(this.message) 40 } 41 } 42} 43``` 44 45## 接口说明 46 47### ReuseIdCallback 48 49ReuseIdCallback用于计算reuseId,类型为() => string。 50 51### ReuseOptions 52 53ReuseOptions保存了reuseId信息,用于.reuse属性中。 54 55| 属性 | 类型 | 说明 | 56| ------- | --------------- | --------------------- | 57| reuseId | ReuseIdCallback | 获取reuseId的回调函数 | 58 59### reuse属性 60 61reuse属性用于\@ReusableV2装饰的自定义组件,给组件指定reuseId。相同reuseId的自定义组件会被互相复用。如果未指定reuseId,则使用自定义组件名作为reuseId。 62 63| 参数 | 类型 | 说明 | 64| ------- | ------------ | ------------------------------------------------------ | 65| options | ReuseOptions | reuse的配置选项,例如{reuseId: () => 'reuseComponent'} | 66 67```ts 68@Entry 69@ComponentV2 70struct Index { 71 build() { 72 Column() { 73 ReusableV2Component() 74 .reuse({reuseId: () => 'reuseComponent'}) // 使用'reuseComponent'作为reuseId 75 ReusableV2Component() 76 .reuse({reuseId: () => ''}) // 使用空字符串将默认使用组件名'ReusableV2Component'作为reuseId 77 ReusableV2Component() // 未指定reuseId将默认使用组件名'ReusableV2Component'作为reuseId 78 } 79 } 80} 81@ReusableV2 82@ComponentV2 83struct ReusableV2Component { 84 build() { 85 } 86} 87``` 88 89## 使用限制 90 91- 仅能将\@ReusableV2装饰的自定义组件作为V2自定义组件的子组件使用。如果在V1的自定义组件中使用V2的复用组件将导致编译期报错,编译期无法校验到的复杂场景下将会有运行时报错。 92 93 ```ts 94 @Entry 95 @ComponentV2 96 struct Index { 97 build() { 98 Column() { 99 ReusableV2Component() // 正确用法 100 V1Component() 101 } 102 } 103 } 104 @ReusableV2 105 @ComponentV2 106 struct ReusableV2Component { 107 build() { 108 } 109 } 110 @Builder 111 function V2ReusableBuilder() { 112 ReusableV2Component() 113 } 114 @Component 115 struct V1Component { 116 build() { 117 Column() { 118 ReusableV2Component() // 错误用法,编译报错 119 V2ReusableBuilder() // 错误用法,较复杂场景,运行时报错 120 } 121 } 122 } 123 ``` 124 125- V1和V2支持部分混用场景。 126 127 下文提到的描述对应关系如下表: 128 129 | 描述 | 对应组件类型 | 130 | ---------- | ------------------------------------ | 131 | V1普通组件 | @Component装饰的struct | 132 | V2普通组件 | @ComponentV2装饰的struct | 133 | V1复用组件 | @Reusable @Component装饰的struct | 134 | V2复用组件 | @ReusableV2 @ComponentV2装饰的struct | 135 136 下面的表展示了V1和V2的混用支持关系,每行的含义为第一列作为父组件,能否将后面列的组件作为子组件。 137 138 以第一行V1普通组件为例,可以将V1普通组件、V2普通组件以及V1复用组件作为子组件,但无法将V2复用组件作为子组件。 139 140 | | V1普通组件 | V2普通组件 | V1复用组件 | V2复用组件 | 141 | ---------- | :--------: | :--------: | :------------------------------------: | :--------------: | 142 | V1普通组件 | 支持 | 支持 | 支持 | 不支持,编译报错 | 143 | V2普通组件 | 支持 | 支持 | 不支持,编译告警,实际使用子组件不创建 | 支持 | 144 | V1复用组件 | 支持 | 支持 | 支持 | 不支持,编译报错 | 145 | V2复用组件 | 支持 | 支持 | 不支持,编译报错 | 支持 | 146 147 根据上表,仅支持12种可能的父子关系,不推荐开发者高度嵌套可复用组件,这会造成复用效率降低。 148 149- V2的复用组件当前不支持直接用于Repeat的template中,但是可以用在template中的V2自定义组件中。 150 151 ```ts 152 @Entry 153 @ComponentV2 154 struct Index { 155 @Local arr: number[] = [1, 2, 3, 4, 5]; 156 build() { 157 Column() { 158 List() { 159 Repeat(this.arr) 160 .each(() => {}) 161 .virtualScroll() 162 .templateId(() => 'a') 163 .template('a', (ri) => { 164 ListItem() { 165 Column() { 166 ReusableV2Component({ val: ri.item}) // 暂不支持,编译期报错 167 ReusableV2Builder(ri.item) // 暂不支持,运行时报错 168 NormalV2Component({ val: ri.item}) // 支持普通V2自定义组件下面包含V2复用组件 169 } 170 } 171 }) 172 } 173 } 174 } 175 } 176 @ComponentV2 177 struct NormalV2Component { 178 @Require @Param val: number; 179 build() { 180 ReusableV2Component({ val: this.val }) 181 } 182 } 183 @Builder 184 function ReusableV2Builder(param: number) { 185 ReusableV2Component({ val: param }) 186 } 187 @ReusableV2 188 @ComponentV2 189 struct ReusableV2Component { 190 @Require @Param val: number; 191 build() { 192 Column() { 193 Text(`val: ${this.val}`) 194 } 195 } 196 } 197 ``` 198 199## 回收与复用的生命周期 200 201\@ReusableV2提供了aboutToRecycle以及aboutToReuse的生命周期,当组件被回收时触发aboutToRecycle,当组件被复用时触发aboutToReuse。 202 203以if的使用场景为例: 204 205```ts 206@Entry 207@ComponentV2 208struct Index { 209 @Local condition1: boolean = false; 210 @Local condition2: boolean = true; 211 build() { 212 Column() { 213 Button('step1. appear') 214 .onClick(() => { 215 this.condition1 = true; 216 }) 217 Button('step2. recycle') 218 .onClick(() => { 219 this.condition2 = false; 220 }) 221 Button('step3. reuse') 222 .onClick(() => { 223 this.condition2 = true; 224 }) 225 Button('step4. disappear') 226 .onClick(() => { 227 this.condition1 = false; 228 }) 229 if (this.condition1) { 230 NormalV2Component({ condition: this.condition2 }) 231 } 232 } 233 } 234} 235@ComponentV2 236struct NormalV2Component { 237 @Require @Param condition: boolean; 238 build() { 239 if (this.condition) { 240 ReusableV2Component() 241 } 242 } 243} 244@ReusableV2 245@ComponentV2 246struct ReusableV2Component { 247 aboutToAppear() { 248 console.log('ReusableV2Component aboutToAppear called'); // 组件创建时调用 249 } 250 aboutToDisappear() { 251 console.log('ReusableV2Component aboutToDisappear called'); // 组件销毁时调用 252 } 253 aboutToRecycle() { 254 console.log('ReusableV2Component aboutToRecycle called'); // 组件回收时调用 255 } 256 aboutToReuse() { 257 console.log('ReusableV2Component aboutToReuse called'); // 组件复用时调用 258 } 259 build() { 260 Column() { 261 Text('ReusableV2Component') 262 } 263 } 264} 265``` 266 267建议按下面顺序进行操作: 268 2691. 点击`step1. appear`,此时`condition1`变为true,`Index`中的if组件切换分支,创建出`NormalV2Component`,由于`condition2`初始值为true,所以`NormalV2Component`中的if条件满足,尝试创建`ReusableV2Component`。此时复用池中无元素,因此会创建`ReusableV2Component`,并回调`aboutToAppear`的方法,输出`ReusableV2Component aboutToAppear called`的日志。 2702. 点击`step2. recycle`,此时`condition2`变为false,通过\@Param同步给`NormalV2Component`,if条件切换,由于`ReusableV2Component`使用了\@ReusableV2,因此会将该组件回收至复用池而不是销毁,回调`aboutToRecycle`的方法并输出`ReusableV2Component aboutToRecycle called`的日志。 2713. 点击`step3. reuse`,此时`condition2`变为true,通过\@Param传递给`NormalV2Component`,if条件切换,由于`ReusableV2Component`使用了\@ReusableV2,因此在创建该组件时尝试去复用池中寻找。此时复用池中有第二步放入的组件实例,因此从复用池中取出复用,回调`aboutToReuse`方法并输出`ReusableV2Component aboutToReuse called`的日志。 2724. 点击`step4. disappear`,此时`condition1`变为false,`Index`组件中的if组件切换分支,销毁`NormalV2Component`,此时`ReusableV2Component`因为父组件销毁,所以会被一起销毁,回调`aboutToDisappear`的方法并输出`ReusableV2Component aboutToDisappear called`的日志。 273 274倘若该复用组件下有子组件时,会在回收和复用时递归调用子组件的aboutToRecycle和aboutToReuse(与子组件是否被标记复用无关),直到遍历完所有的孩子组件。 275 276## 复用阶段的冻结 277 278在之前的复用中,V1组件在复用池中仍能响应更新,这会对性能带来一定的负面影响,需要开发者使用组件冻结能力,才能够使V1组件在复用池中时不响应更新。针对这一点,V2组件在复用时将会被自动冻结,不会响应在回收期间发生的变化。这一个期间包括`aboutToRecycle`,即`aboutToRecycle`中的修改不会刷新到UI上,也不会触发\@Computed以及\@Monitor。冻结状态将持续到aboutToReuse前,即`aboutToReuse`及之后的变量更改,才会正常触发UI刷新、\@Computed重新计算以及\@Monitor的调用。 279 280以if的使用场景为例: 281 282```ts 283@ObservedV2 284class Info { 285 @Trace age: number = 25; 286} 287const info: Info = new Info(); 288@Entry 289@ComponentV2 290struct Index { 291 @Local condition: boolean = true; 292 build() { 293 Column() { 294 Button('复用/回收').onClick(()=>{this.condition=!this.condition;}) 295 Button('改值').onClick(()=>{info.age++;}) 296 if (this.condition) { 297 ReusableV2Component() 298 } 299 } 300 } 301} 302@ReusableV2 303@ComponentV2 304struct ReusableV2Component { 305 @Local info: Info = info; // 仅做演示使用,并不建议@Local赋值全局变量 306 @Monitor('info.age') 307 onValChange() { 308 console.log('info.age change'); 309 } 310 aboutToRecycle() { 311 console.log('aboutToRecycle'); 312 this.info.age++; 313 } 314 aboutToReuse() { 315 console.log('aboutToReuse'); 316 this.info.age++; 317 } 318 onRender(): string { 319 console.log('info.age onRender') 320 return this.info.age.toString(); 321 } 322 build() { 323 Column() { 324 Text(this.onRender()) 325 } 326 } 327} 328``` 329 330建议按如下步骤进行操作: 331 3321. 点击`改值`按钮,可以观察到UI变化,\@Monitor触发并输出日志`info.age change`以及`info.age onRender`,说明此时能够正常监听到变化以及触发UI刷新。 3332. 点击`复用/回收`按钮,此时调用`aboutToRecycle`回调并输出`aboutToRecycle`的日志,但\@Monitor不被触发,且`onRender`方法不被回调。 3343. 点击`改值`按钮,UI无变化,\@Monitor不触发且`onRender`方法不被回调。 3354. 点击`复用/回收`按钮,此时UI变化,\@Monitor触发并输出日志`info.age change`且`onRender`方法回调输出`info.age onRender`。 336 337如果去掉`aboutToReuse`方法中的自增操作,则上述第四步不会触发\@Monitor回调。 338 339在复杂的混用场景中,是否冻结的规则可以总结为以下两点: 340 3411. V1的组件根据是否开启组件冻结freezeWhenInactive决定。 3422. V2的组件自动被冻结。 343 344## 复用前的组件内状态变量重置 345 346与\@Reusable不同的是,\@ReusableV2在复用前会重置组件中的状态变量以及相关的\@Computed、\@Monitor的内容。在复用的过程当中,所有的V2自定义组件,无论是否被标记了\@ReusableV2,都会经历这一个重置过程。 347 348重置会按照变量在组件中定义的顺序按照下面的规则依次进行: 349 350| 装饰器 | 重置方法 | 351| ---------- | ------------------------------------------------------------ | 352| \@Local | 直接使用定义时的初始值重新赋值。 | 353| \@Param | 如果有外部传入则使用外部传入值重新赋值,否则用本地初始值重新赋值。注意:\@Once装饰的变量同样会被重置初始化一次。 | 354| \@Event | 如果有外部传入则使用外部传入值重新赋值,否则用本地初始值重新赋值。如果本地没有初始值,则生成默认的空实现。 | 355| \@Provider | 直接使用定义时的初始值重新赋值。 | 356| \@Consumer | 如果有对应的\@Provider则直接使用\@Provider对应的值,否则使用本地初始值重新赋值。 | 357| \@Computed | 使用当前最新的值重新计算一次,如果使用到的变量还未被重置,将会使用重置前的值,因此推荐开发者将\@Computed定义在所使用的变量之后。 | 358| \@Monitor | 在上述所有变量重置完成之后触发。重置过程中产生的变量变化不会触发\@Monitor回调,仅更新IMonitorValue中的before值。重置过程中不产生变化的赋值不会触发\@Monitor的重置。 | 359| 常量 | 包括readonly的常量,不重置。 | 360 361下面的例子展示了重置的一些效果: 362 363```ts 364@ObservedV2 365class Info { 366 @Trace age: number; 367 constructor(age: number) { 368 this.age = age; 369 } 370} 371@Entry 372@ComponentV2 373struct Index { 374 @Local local: number = 0; 375 @Provider('inherit') inheritProvider: number = 100; 376 @Local condition: boolean = true; 377 build() { 378 Column() { 379 Button('回收/复用').onClick(()=>{this.condition=!this.condition;}) 380 Column() { 381 Text('父组件变量') 382 Text(`local: ${this.local}`).onClick(()=>{this.local++;}) 383 Text(`inheritProvider: ${this.inheritProvider}`).onClick(()=>{this.inheritProvider++;}) 384 }.borderWidth(2) 385 if (this.condition) { 386 ReusableV2Component({ 387 paramOut: this.local, 388 paramOnce: this.local, 389 changeParam: () => { 390 this.local++; 391 } 392 }) 393 } 394 } 395 } 396} 397@ReusableV2 398@ComponentV2 399struct ReusableV2Component { 400 @Local val: number = 0; 401 @Local info: Info = new Info(25); 402 @Param paramLocal: number = 1; 403 @Require @Param paramOut: number; 404 @Require @Param @Once paramOnce: number; 405 @Event changeParam: () => void; 406 @Provider('selfProvider') selfProvider: number = 0; 407 @Consumer('inherit') inheritConsumer: number = 0; 408 @Consumer('selfConsumer') selfConsumer: number = 0; 409 noDecoVariable: number = 0; // 未加装饰器,被视作常量 410 noDecoInfo: Info = new Info(30); // 未加装饰器,被视作常量 411 readonly readOnlyVariable: number = 0; // readonly常量 412 @Computed 413 get plusParam() { 414 return this.paramLocal + this.paramOut + this.paramOnce; 415 } 416 @Monitor('val') 417 onValChange(monitor: IMonitor) { 418 console.log(`val change from ${monitor.value()?.before} to ${monitor.value()?.now}`); 419 } 420 @Monitor('plusParam') 421 onPlusParamChange(monitor: IMonitor) { 422 console.log(`plusParam change from ${monitor.value()?.before} to ${monitor.value()?.now}`); 423 } 424 build() { 425 Column() { 426 Column() { 427 Text('重置为本地初始值的变量') 428 Text(`val: ${this.val}`).onClick(()=>{this.val++;}) 429 Text(`info.age: ${this.info.age}`).onClick(()=>{this.info.age++;}) 430 Text(`paramLocal: ${this.paramLocal}`).onClick(()=>{/* 无外部传入的Local无法本地修改 */}) 431 Text(`selfProvider: ${this.selfProvider}`).onClick(()=>{this.selfProvider++;}) 432 Text(`selfConsumer: ${this.selfConsumer}`).onClick(()=>{this.selfConsumer++;}) 433 }.borderWidth(2) 434 Column() { 435 Text('重置为外部传入的变量') 436 Text(`paramOut: ${this.paramOut}`).onClick(()=>{this.changeParam();}) 437 Text(`paramOnce: ${this.paramOnce}`).onClick(()=>{this.paramOnce++;}) 438 }.borderWidth(2) 439 Column() { 440 Text('根据父组件情况决定') 441 Text(`inheritConsumer: ${this.inheritConsumer}`).onClick(()=>{this.inheritConsumer++;}) 442 Text(`plusParam: ${this.plusParam}`) 443 }.borderWidth(2) 444 Column() { 445 Text('不被重置') 446 Text(`noDecoVariable: ${this.noDecoVariable}`) 447 Text(`noDecoInfo.age: ${this.noDecoInfo.age}`).onClick(()=>{this.noDecoInfo.age++;}) // 能够触发刷新但是复用时不会被重置 448 Text(`readOnlyVariable: ${this.readOnlyVariable}`) 449 }.borderWidth(2) 450 } 451 } 452} 453``` 454 455开发者可以尝试点击各个变量,并点击`回收/复用`按钮查看复用后的重置情况。 456 457需要注意的是,上面的例子中`noDecoInfo`未被重置,如果存在监听`noDecoInfo.age`的\@Monitor,因为noDecoInfo本身未产生变化,所以该\@Monitor也不会被重置,因此在后续第一次更改`noDecoInfo.age`时,`IMonitorValue`的`before`值将不会被重置,仍是复用前的值。 458 459将上面的例子简化可得下面的例子: 460 461```ts 462@ObservedV2 463class Info { 464 @Trace age: number; 465 constructor(age: number) { 466 this.age = age; 467 } 468} 469@Entry 470@ComponentV2 471struct Index { 472 @Local condition: boolean = true; 473 build() { 474 Column() { 475 Button('回收/复用').onClick(()=>{this.condition=!this.condition;}) 476 if (this.condition) { 477 ReusableV2Component() 478 } 479 } 480 } 481} 482@ReusableV2 483@ComponentV2 484struct ReusableV2Component { 485 noDecoInfo: Info = new Info(30); // 未加装饰器,被视作常量 486 @Monitor('noDecoInfo.age') 487 onAgeChange(monitor: IMonitor) { 488 console.log(`age change from ${monitor.value()?.before} to ${monitor.value()?.now}`); 489 } 490 aboutToRecycle() { 491 this.noDecoInfo.age = 25; 492 } 493 aboutToReuse() { 494 this.noDecoInfo.age = 35; 495 } 496 build() { 497 Column() { 498 Column() { 499 Text(`noDecoInfo.age: ${this.noDecoInfo.age}`) 500 .onClick(()=>{this.noDecoInfo.age++;}) // 能够触发刷新但是不会被重置 501 } 502 } 503 } 504} 505``` 506 507建议按照下列步骤进行操作: 508 5091. 点击`noDecoInfo.age: 30`,UI刷新为`noDecoInfo.age: 31`,\@Monitor触发并输出日志`age change from 30 to 31`。 5102. 点击`回收/复用`两次,UI刷新为`noDecoInfo.age: 35`,\@Monitor触发并输出日志`age change from 31 to 35`。 5113. 点击`noDecoInfo.age: 35`,UI刷新为`noDecoInfo.age: 36`,\@Monitor触发并输出日志`age change from 35 to 36`。 512 513由于冻结机制的存在,在aboutToRecycle中赋值不会被\@Monitor观察到。而在经历完变量重置后,变量又会被赋予新的值,因此对于组件内状态变量来说,在aboutToRecycle中赋值不会有明显的效果;而常量(例如上面的`noDecoInfo`)由于冻结机制的存在,在aboutToRecycle中更改`age`也不会被观察到,并且因为不会被重置,所以相关的\@Monitor也不会被重置,即这里的`age`值本身未被重置,也就不会重置与之绑定的\@Monitor。最终表现出来的现象即:第二步回调的\@Monitor中,`monitor.value()?.before`得到的值为31,而非age的初始值30。 514 515针对这一现象,推荐开发者在复用的场景减少使用类似的常量对象包含\@Trace属性的写法,以确保复用场景的功能符合预期。 516 517## 使用场景 518 519### 在if组件中使用 520 521通过改变if组件的条件可以控制组件回收/复用。 522 523```ts 524@Entry 525@ComponentV2 526struct Index { 527 @Local condition: boolean = true; 528 build() { 529 Column() { 530 Button('回收/复用').onClick(()=>{this.condition=!this.condition;}) // 点击切换回收/复用状态 531 if (this.condition) { 532 ReusableV2Component() 533 } 534 } 535 } 536} 537@ReusableV2 538@ComponentV2 539struct ReusableV2Component { 540 @Local message: string = 'Hello World'; 541 aboutToRecycle() { 542 console.log('ReusableV2Component aboutToRecycle'); // 回收时被调用 543 } 544 aboutToReuse() { 545 console.log('ReusableV2Component aboutToReuse'); // 复用时被调用 546 } 547 build() { 548 Column() { 549 Text(this.message) 550 } 551 } 552} 553``` 554 555### 在Repeat组件non-virtualScroll场景的each属性中使用 556 557Repeat组件non-virtualScroll场景中,会在删除/创建子树时触发回收/复用。 558 559```ts 560@Entry 561@ComponentV2 562struct Index { 563 @Local simpleList: number[] = [1, 2, 3, 4, 5]; 564 @Local condition: boolean = true; 565 build() { 566 Column() { 567 Button('删除/创建Repeat').onClick(()=>{this.condition=!this.condition;}) 568 Button('增加元素').onClick(()=>{this.simpleList.push(this.simpleList.length+1);}) 569 Button('删除元素').onClick(()=>{this.simpleList.pop();}) 570 Button('更改元素').onClick(()=>{this.simpleList[0]++;}) 571 if (this.condition) { 572 List({ space: 10 }) { 573 Repeat(this.simpleList) 574 .each((obj: RepeatItem<number>) => { 575 ListItem() { 576 Column() { 577 ReusableV2Component({ num: obj.item }) 578 } 579 } 580 }) 581 } 582 } 583 } 584 } 585} 586@ReusableV2 587@ComponentV2 588struct ReusableV2Component { 589 @Require @Param num: number; 590 aboutToAppear() { 591 console.log('ReusableV2Component aboutToAppear'); 592 } 593 aboutToRecycle() { 594 console.log('ReusableV2Component aboutToRecycle'); 595 } 596 aboutToReuse() { 597 console.log('ReusableV2Component aboutToReuse'); 598 } 599 build() { 600 Column() { 601 Text(`${this.num}`) 602 } 603 } 604} 605``` 606 607### 在Repeat组件virtualScroll场景的each属性中使用 608 609Repeat组件virtualScroll场景中,将会优先使用Repeat组件的缓存池,正常滑动场景、更新场景不涉及组件的回收与复用。当Repeat的缓存池需要扩充时将会向自定义组件要求新的子组件,此时如果复用池中有可复用的节点,将会进行复用。 610 611下面的例子中,先点击`改变condition`会让3个节点进入复用池,而后向下滑动List组件时,可以观察到日志输出`ReusableV2Component aboutToReuse`,表明Repeat可以使用自定义组件的复用池填充自己的缓存池。 612 613```ts 614@Entry 615@ComponentV2 616struct Index { 617 @Local condition: boolean = true; 618 @Local simpleList: number[] = []; 619 aboutToAppear(): void { 620 for (let i = 0; i < 100; i++) { 621 this.simpleList.push(i) 622 } 623 } 624 build() { 625 Column() { 626 Button('改变condition').onClick(()=>{this.condition=!this.condition;}) 627 if (this.condition) { 628 // 此处仅做演示使用,让复用池中填充3个组件 629 ReusableV2Component({ num: 0}) 630 ReusableV2Component({ num: 0}) 631 ReusableV2Component({ num: 0}) 632 } 633 List({ space: 10 }) { 634 Repeat(this.simpleList) 635 .virtualScroll() 636 .each((obj: RepeatItem<number>) => { 637 ListItem() { 638 Column() { 639 ReusableV2Component({ num: obj.item }) 640 } 641 } 642 }) 643 }.height('50%') 644 .cachedCount(2) 645 } 646 } 647} 648@ReusableV2 649@ComponentV2 650struct ReusableV2Component { 651 @Require @Param num: number; 652 aboutToAppear() { 653 console.log('ReusableV2Component aboutToAppear'); 654 } 655 aboutToRecycle() { 656 console.log('ReusableV2Component aboutToRecycle'); 657 } 658 aboutToReuse() { 659 console.log('ReusableV2Component aboutToReuse'); 660 } 661 build() { 662 Column() { 663 Text(`${this.num}`).fontSize(50) 664 } 665 } 666} 667``` 668 669### 在ForEach组件中使用 670>**说明:** 671> 672>推荐开发者使用Repeat组件的non-virtualScroll场景代替ForEach组件 673 674下面的例子中使用了ForEach组件渲染了数个可复用组件,由于每次点击`点击修改`按钮时key值都会发生变化,因此从第二次点击开始都会触发回收与复用(由于ForEach先判断有无可复用节点时复用池仍未初始化,因此第一次点击会创建新的节点,而后初始化复用池同时回收节点)。 675 676```ts 677@Entry 678@ComponentV2 679struct Index { 680 @Local simpleList: number[] = [0, 1, 2, 3, 4, 5]; 681 build() { 682 Column() { 683 ForEach(this.simpleList, (num: number, index) => { 684 Row() { 685 Button('点击修改').onClick(()=>{this.simpleList[index]++;}) 686 ReusableV2Component({ num: num }) 687 } 688 }) // 每次修改完key发生变化 689 } 690 } 691} 692@ReusableV2 693@ComponentV2 694struct ReusableV2Component { 695 @Require @Param num: number; 696 aboutToAppear() { 697 console.log('ReusableV2Component aboutToAppear', this.num); // 创建时触发 698 } 699 aboutToRecycle() { 700 console.log('ReusableV2Component aboutToRecycle', this.num); // 回收时触发 701 } 702 aboutToReuse() { 703 console.log('ReusableV2Component aboutToReuse', this.num); // 复用时触发 704 } 705 build() { 706 Column() { 707 Text(`child: ${this.num}`) 708 } 709 } 710} 711``` 712 713 714### 在LazyForEach组件中使用 715>**说明:** 716> 717>推荐开发者使用Repeat组件的virtualScroll场景代替LazyForEach组件 718 719下面的例子中使用了LazyForEach渲染了数个可复用组件,在滑动时可以先观察到组件创建,直到预加载节点全部创建完成之后,再滑动则触发复用和回收。 720 721```ts 722class BasicDataSource implements IDataSource { 723 private listeners: DataChangeListener[] = []; 724 private originDataArray: StringData[] = []; 725 726 public totalCount(): number { 727 return 0; 728 } 729 730 public getData(index: number): StringData { 731 return this.originDataArray[index]; 732 } 733 734 registerDataChangeListener(listener: DataChangeListener): void { 735 if (this.listeners.indexOf(listener) < 0) { 736 console.info('add listener'); 737 this.listeners.push(listener); 738 } 739 } 740 741 unregisterDataChangeListener(listener: DataChangeListener): void { 742 const pos = this.listeners.indexOf(listener); 743 if (pos >= 0) { 744 console.info('remove listener'); 745 this.listeners.splice(pos, 1); 746 } 747 } 748 749 notifyDataReload(): void { 750 this.listeners.forEach(listener => { 751 listener.onDataReloaded(); 752 }) 753 } 754 755 notifyDataAdd(index: number): void { 756 this.listeners.forEach(listener => { 757 listener.onDataAdd(index); 758 }) 759 } 760 761 notifyDataChange(index: number): void { 762 this.listeners.forEach(listener => { 763 listener.onDataChange(index); 764 }) 765 } 766 767 notifyDataDelete(index: number): void { 768 this.listeners.forEach(listener => { 769 listener.onDataDelete(index); 770 }) 771 } 772 773 notifyDataMove(from: number, to: number): void { 774 this.listeners.forEach(listener => { 775 listener.onDataMove(from, to); 776 }) 777 } 778 779 notifyDatasetChange(operations: DataOperation[]): void { 780 this.listeners.forEach(listener => { 781 listener.onDatasetChange(operations); 782 }) 783 } 784} 785 786class MyDataSource extends BasicDataSource { 787 private dataArray: StringData[] = []; 788 789 public totalCount(): number { 790 return this.dataArray.length; 791 } 792 793 public getData(index: number): StringData { 794 return this.dataArray[index]; 795 } 796 797 public addData(index: number, data: StringData): void { 798 this.dataArray.splice(index, 0, data); 799 this.notifyDataAdd(index); 800 } 801 802 public pushData(data: StringData): void { 803 this.dataArray.push(data); 804 this.notifyDataAdd(this.dataArray.length - 1); 805 } 806} 807 808@ObservedV2 809class StringData { 810 @Trace message: string; 811 constructor(message: string) { 812 this.message = message; 813 } 814} 815 816@Entry 817@ComponentV2 818struct Index { 819 data: MyDataSource = new MyDataSource(); // 数据源 820 821 aboutToAppear() { 822 for (let i = 0; i <= 200; i++) { 823 this.data.pushData(new StringData('Hello' + i)); 824 } 825 } 826 build() { 827 List({ space: 3 }) { 828 LazyForEach(this.data, (item: StringData, index: number) => { 829 ListItem() { 830 Column() { 831 Text(item.message) 832 ChildComponent({ data: item.message }) 833 .onClick(() => { 834 item.message += '!'; // message为@Trace装饰的变量,可观察变化 835 }) 836 } 837 } 838 }) 839 }.cachedCount(5) 840 } 841} 842 843@ReusableV2 844@ComponentV2 845struct ChildComponent { 846 @Param @Require data: string; 847 aboutToAppear(): void { 848 console.log('ChildComponent aboutToAppear', this.data) 849 } 850 aboutToDisappear(): void { 851 console.log('ChildComponent aboutToDisappear', this.data) 852 } 853 aboutToReuse(): void { 854 console.log('ChildComponent aboutToReuse', this.data) // 复用时触发 855 } 856 aboutToRecycle(): void { 857 console.log('ChildComponent aboutToRecycle', this.data) // 回收时触发 858 } 859 build() { 860 Row() { 861 Text(this.data).fontSize(50) 862 } 863 } 864} 865``` 866 867