@ReusableV2装饰器:V2组件复用

为了降低反复创建销毁自定义组件带来的性能开销,开发者可以使用@ReusableV2装饰@ComponentV2装饰的自定义组件,达成组件复用的效果。

在阅读本文前,建议提前阅读:@Reusable装饰器:V1组件复用

说明:

从API version 18开始,可以使用@ReusableV2装饰@ComponentV2装饰的自定义组件。

从API version 18开始,该装饰器支持在原子化服务中使用。

概述

@ReusableV2用于装饰V2的自定义组件,表明该自定义组件具有被复用的能力:

  • @ReusableV2仅能装饰V2的自定义组件,即@ComponentV2装饰的自定义组件。并且仅能将@ReusableV2装饰的自定义组件作为V2自定义组件的子组件使用。
  • @ReusableV2同样提供了aboutToRecycleaboutToReuse的生命周期,在组件被回收时调用aboutToRecycle,在组件被复用时调用aboutToReuse,但与@Reusable不同的是,aboutToReuse没有入参。
  • 在回收阶段,会递归地调用所有子组件的aboutToRecycle回调(即使子组件未被标记可复用);在复用阶段,会递归地调用所有子组件的aboutToReuse回调(即使子组件未被标记可复用)。
  • @ReusableV2装饰的自定义组件会在被回收期间保持冻结状态,即无法触发UI刷新、无法触发@Monitor回调,与freezeWhenInactive标记位不同的是,在解除冻结状态后,不会触发延后的刷新。
  • @ReusableV2装饰的自定义组件会在复用时自动重置组件内状态变量的值、重新计算组件内@Computed以及与之相关的@Monitor。不建议开发者在aboutToRecycle中更改组件内状态变量,详见复用前的组件内状态变量重置
  • V1和V2的复用组件可在一定规则下混用,详见使用限制第二点。
  • 不建议开发者嵌套滥用@ReusableV2装饰器,这可能会导致复用效率降低以及内存开销变大。

装饰器说明

@ReusableV2装饰器 说明
装饰器参数
可装饰的组件 @ComponentV2装饰的自定义组件
装饰器作用 表明该组件可被复用
@ReusableV2 // 装饰ComponentV2的自定义组件
@ComponentV2
struct ReusableV2Component {
  @Local message: string = 'Hello World';
  build () {
    Column() {
      Text(this.message)
    }
  }
}

接口说明

reuse、ReuseOptions、ReuseIdCallback的接口说明参考API文档:复用选项

@Entry
@ComponentV2
struct Index {
  build() {
    Column() {
      ReusableV2Component()
        .reuse({ reuseId: () => 'reuseComponent' }) // 使用'reuseComponent'作为reuseId
      ReusableV2Component()
        .reuse({ reuseId: () => '' }) // 使用空字符串将默认使用组件名'ReusableV2Component'作为reuseId
      ReusableV2Component() // 未指定reuseId将默认使用组件名'ReusableV2Component'作为reuseId
    }
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  build() {
  }
}

使用限制

  • 仅能将@ReusableV2装饰的自定义组件作为V2自定义组件的子组件使用。如果在V1的自定义组件中使用V2的复用组件将导致编译期报错,编译期无法校验到的复杂场景下将会有运行时报错。

    @Entry
    @ComponentV2
    struct Index {
      build() {
        Column() {
          ReusableV2Component() // 正确用法
        }
      }
    }
    
    @ReusableV2
    @ComponentV2
    struct ReusableV2Component {
      build() {
      }
    }
    
    @Builder
    function V2ReusableBuilder() {
      ReusableV2Component()
    }
    
  • V1和V2支持部分混用场景。

    下文提到的描述对应关系如下表:

    描述 对应组件类型
    V1普通组件 @Component装饰的struct。
    V2普通组件 @ComponentV2装饰的struct。
    V1复用组件 @Reusable@Component装饰的struct。
    V2复用组件 @ReusableV2@ComponentV2装饰的struct。

    下面的表展示了V1和V2的混用支持关系,每行的含义为第一列作为父组件,能否将后面列的组件作为子组件。

    以第一行V1普通组件为例,可以将V1普通组件、V2普通组件以及V1复用组件作为子组件,但无法将V2复用组件作为子组件。

    混用支持关系 V1普通组件 V2普通组件 V1复用组件 V2复用组件
    V1普通组件 支持 支持 支持 不支持,编译报错
    V2普通组件 支持 支持 不支持,编译告警,实际使用子组件不创建 支持
    V1复用组件 支持 支持,需要使用API version 18及以上的SDK,否则会有运行时报错,从API version 23开始,将返回错误码140113 支持 不支持,编译报错
    V2复用组件 支持 支持 不支持,编译报错 支持

    根据上表,仅支持12种可能的父子关系,不推荐开发者高度嵌套可复用组件,这会造成复用效率降低。

  • V2的复用组件当前不支持直接用于Repeat的template中,但是可以用在template中的V2自定义组件中。

    @Entry
    @ComponentV2
    struct Index {
      @Local arr: number[] = [1, 2, 3, 4, 5];
    
      build() {
        Column() {
          List() {
            Repeat(this.arr)
              .each(() => {
              })
              .virtualScroll()
              .templateId(() => 'a')
              .template('a', (ri) => {
                ListItem() {
                  Column() {
                    NormalV2Component({ val: ri.item }) // 支持普通V2自定义组件下面包含V2复用组件
                  }
                }
              })
          }
        }
      }
    }
    
    @ComponentV2
    struct NormalV2Component {
      @Require @Param val: number;
    
      build() {
        ReusableV2Component({ val: this.val })
      }
    }
    
    @Builder
    function ReusableV2Builder(param: number) {
      ReusableV2Component({ val: param })
    }
    
    @ReusableV2
    @ComponentV2
    struct ReusableV2Component {
      @Require @Param val: number;
    
      build() {
        Column() {
          Text(`val: ${this.val}`)
        }
      }
    }
    

回收与复用的生命周期

@ReusableV2提供了aboutToRecycle以及aboutToReuse的生命周期,当组件被回收时触发aboutToRecycle,当组件被复用时触发aboutToReuse。

以if的使用场景为例:

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_Reusablev2]';
const DOMAIN = 0xF811;

@Entry
@ComponentV2
struct Index {
  @Local condition1: boolean = false;
  @Local condition2: boolean = true;

  build() {
    Column({ space: 10 }) {
      Button('step1. appear')
        .width('60%')
        .onClick(() => {
          this.condition1 = true;
        })
      Button('step2. recycle')
        .width('60%')
        .onClick(() => {
          this.condition2 = false;
        })
      Button('step3. reuse')
        .width('60%')
        .onClick(() => {
          this.condition2 = true;
        })
      Button('step4. disappear')
        .width('60%')
        .onClick(() => {
          this.condition1 = false;
        })
      if (this.condition1) {
        NormalV2Component({ condition: this.condition2 })
      }
    }
    .width('100%')
  }
}

@ComponentV2
struct NormalV2Component {
  @Require @Param condition: boolean;

  build() {
    if (this.condition) {
      ReusableV2Component()
    }
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  aboutToAppear() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToAppear called'); // 组件创建时调用
  }

  aboutToDisappear() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToDisappear called'); // 组件销毁时调用
  }

  aboutToRecycle() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToRecycle called'); // 组件回收时调用
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToReuse called'); // 组件复用时调用
  }

  build() {
    Column() {
      Text('ReusableV2Component')
    }
  }
}

建议按下面顺序进行操作:

  1. 点击step1. appear,此时condition1变为true,Index中的if组件切换分支,创建出NormalV2Component,由于condition2初始值为true,所以NormalV2Component中的if条件满足,尝试创建ReusableV2Component。此时复用池中无元素,因此会创建ReusableV2Component,并回调aboutToAppear的方法,输出ReusableV2Component aboutToAppear called的日志。
  2. 点击step2. recycle,此时condition2变为false,通过@Param同步给NormalV2Component,if条件切换,由于ReusableV2Component使用了@ReusableV2,因此会将该组件回收至复用池而不是销毁,回调aboutToRecycle的方法并输出ReusableV2Component aboutToRecycle called的日志。
  3. 点击step3. reuse,此时condition2变为true,通过@Param传递给NormalV2Component,if条件切换,由于ReusableV2Component使用了@ReusableV2,因此在创建该组件时尝试去复用池中寻找。此时复用池中有第二步放入的组件实例,因此从复用池中取出复用,回调aboutToReuse方法并输出ReusableV2Component aboutToReuse called的日志。
  4. 点击step4. disappear,此时condition1变为false,Index组件中的if组件切换分支,销毁NormalV2Component,此时ReusableV2Component因为父组件销毁,所以会被一起销毁,回调aboutToDisappear的方法并输出ReusableV2Component aboutToDisappear called的日志。

倘若该复用组件下有子组件时,会在回收和复用时递归调用子组件的aboutToRecycle和aboutToReuse(与子组件是否被标记复用无关),直到遍历完所有的孩子组件。

lifecycle

复用阶段的冻结

在之前的复用中,V1组件在复用池中仍能响应更新,这会对性能带来一定的负面影响,需要开发者使用组件冻结能力,才能够使V1组件在复用池中时不响应更新。针对这一点,V2组件在复用时将会被自动冻结,不会响应在回收期间发生的变化。这一个期间包括aboutToRecycle,即aboutToRecycle中的修改不会刷新到UI上,也不会触发@Computed以及@Monitor。冻结状态将持续到aboutToReuse前,即aboutToReuse及之后的变量更改,才会正常触发UI刷新、@Computed重新计算以及@Monitor的调用。

以if的使用场景为例:

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_Reusablev2]';
const DOMAIN = 0xF811;

@ObservedV2
class Info {
  @Trace public age: number = 25;
}

const info: Info = new Info();

@Entry
@ComponentV2
struct Index {
  @Local condition: boolean = true;

  build() {
    Column({ space: 10 }) {
      Button('Reuse/Recycle')
        .width('60%')
        .onClick(() => {
          this.condition = !this.condition;
        })
      Button('Change value')
        .width('60%')
        .onClick(() => {
          info.age++;
        })
      if (this.condition) {
        ReusableV2Component()
      }
    }
    .width('100%')
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Local info: Info = info; // 仅做演示使用,并不建议@Local赋值全局变量

  @Monitor('info.age')
  onValChange() {
    hilog.info(DOMAIN, TAG, 'info.age change');
  }

  aboutToRecycle() {
    hilog.info(DOMAIN, TAG, 'aboutToRecycle');
    this.info.age++;
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, 'aboutToReuse');
    this.info.age++;
  }

  onRender(): string {
    hilog.info(DOMAIN, TAG, 'info.age onRender');
    return this.info.age.toString();
  }

  build() {
    Column() {
      Text(this.onRender())
    }
  }
}

建议按如下步骤进行操作:

  1. 点击Change value按钮,可以观察到UI变化,@Monitor触发并输出日志info.age change以及info.age onRender,说明此时能够正常监听到变化以及触发UI刷新。
  2. 点击Reuse/Recycle按钮,此时调用aboutToRecycle回调并输出aboutToRecycle的日志,但@Monitor不被触发,且onRender方法不被回调。
  3. 点击Change value按钮,UI无变化,@Monitor不触发且onRender方法不被回调。
  4. 点击Reuse/Recycle按钮,此时调用aboutToReuse回调并输出aboutToReuse的日志,@Monitor触发并输出日志info.age changeonRender方法回调输出info.age onRender,UI发生变化。

freeze

如果去掉aboutToReuse方法中的自增操作,则上述第四步不会触发@Monitor回调。

在复杂的混用场景中,是否冻结的规则可以总结为以下两点:

  1. V1的组件根据是否开启组件冻结freezeWhenInactive决定。
  2. V2的组件自动被冻结。

复用前的组件内状态变量重置

与@Reusable不同的是,@ReusableV2在复用前会重置组件中的状态变量以及相关的@Computed、@Monitor的内容。在复用的过程当中,所有的V2自定义组件,无论是否被标记了@ReusableV2,都会经历这一个重置过程。

重置会按照变量在组件中定义的顺序按照下面的规则依次进行:

装饰器 重置方法
@Local 直接使用定义时的初始值重新赋值。
@Param 如果有外部传入则使用外部传入值重新赋值,否则用本地初始值重新赋值。注意:@Once装饰的变量同样会被重置初始化一次。
@Event 如果有外部传入则使用外部传入值重新赋值,否则用本地初始值重新赋值。如果本地没有初始值,则生成默认的空实现。
@Provider 直接使用定义时的初始值重新赋值。
@Consumer 如果有对应的@Provider则直接使用@Provider对应的值,否则使用本地初始值重新赋值。
@Computed 使用当前最新的值重新计算一次,如果使用到的变量还未被重置,将会使用重置前的值,因此推荐开发者将@Computed定义在所使用的变量之后。
@Monitor 在上述所有变量重置完成之后触发。重置过程中产生的变量变化不会触发@Monitor回调,仅更新IMonitorValue中的before值。重置过程中不产生变化的赋值不会触发@Monitor的重置。
常量 包括readonly的常量,不重置。

下面的例子展示了重置的一些效果:

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_Reusablev2]';
const DOMAIN = 0xF811;

@ObservedV2
class Info {
  @Trace public age: number;

  constructor(age: number) {
    this.age = age;
  }
}

@Entry
@ComponentV2
struct Index {
  @Local local: number = 0;
  @Provider('inherit') inheritProvider: number = 100;
  @Local condition: boolean = true;

  build() {
    Column({ space: 10 }) {
      Button('Recycle/Reuse')
        .onClick(() => {
          this.condition = !this.condition;
        })
      Column({ space: 10 }) {
        Text('Variables of parent component')
        Text(`local: ${this.local}`)
          .onClick(() => {
            this.local++;
          })
        Text(`inheritProvider: ${this.inheritProvider}`)
          .onClick(() => {
            this.inheritProvider++;
          })
      }
      .width('80%')
      .borderWidth(2)

      if (this.condition) {
        ReusableV2Component({
          paramOut: this.local,
          paramOnce: this.local,
          changeParam: () => {
            this.local++;
          }
        })
      }
    }
    .width('100%')
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Local val: number = 0;
  @Local info: Info = new Info(25);
  @Param paramLocal: number = 1;
  @Require @Param paramOut: number;
  @Require @Param @Once paramOnce: number;
  @Event changeParam: () => void;
  @Provider('selfProvider') selfProvider: number = 0;
  @Consumer('inherit') inheritConsumer: number = 0;
  @Consumer('selfConsumer') selfConsumer: number = 0;
  noDecoVariable: number = 0; // 未加装饰器,被视作常量
  noDecoInfo: Info = new Info(30); // 未加装饰器,被视作常量
  readonly readOnlyVariable: number = 0; // readonly常量

  @Computed
  get plusParam() {
    return this.paramLocal + this.paramOut + this.paramOnce;
  }

  @Monitor('val')
  onValChange(monitor: IMonitor) {
    hilog.info(DOMAIN, TAG, `val change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
  }

  @Monitor('plusParam')
  onPlusParamChange(monitor: IMonitor) {
    hilog.info(DOMAIN, TAG, `plusParam change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
  }

  build() {
    Column({ space: 10 }) {
      Column({ space: 10 }) {
        Text('Variables reset to local initial values')
        Text(`val: ${this.val}`)
          .onClick(() => {
            this.val++;
          })
        Text(`info.age: ${this.info.age}`)
          .onClick(() => {
            this.info.age++;
          })
        Text(`paramLocal: ${this.paramLocal}`)
          .onClick(() => {
            /* 无外部传入的Local无法本地修改 */
          })
        Text(`selfProvider: ${this.selfProvider}`)
          .onClick(() => {
            this.selfProvider++;
          })
        Text(`selfConsumer: ${this.selfConsumer}`)
          .onClick(() => {
            this.selfConsumer++;
          })
      }
      .width('80%')
      .borderWidth(2)

      Column({ space: 10 }) {
        Text('Reset to an external variable')
        Text(`paramOut: ${this.paramOut}`)
          .onClick(() => {
            this.changeParam();
          })
        Text(`paramOnce: ${this.paramOnce}`)
          .onClick(() => {
            this.paramOnce++;
          })
      }
      .width('80%')
      .borderWidth(2)

      Column({ space: 10 }) {
        Text('Depending on the parent component')
        Text(`inheritConsumer: ${this.inheritConsumer}`)
          .onClick(() => {
            this.inheritConsumer++;
          })
        Text(`plusParam: ${this.plusParam}`)
      }
      .width('80%')
      .borderWidth(2)

      Column({ space: 10 }) {
        Text('Not reset')
        Text(`noDecoVariable: ${this.noDecoVariable}`)
        Text(`noDecoInfo.age: ${this.noDecoInfo.age}`)
          .onClick(() => {
            this.noDecoInfo.age++;
          }) // 能够触发刷新但是复用时不会被重置
        Text(`readOnlyVariable: ${this.readOnlyVariable}`)
      }
      .width('80%')
      .borderWidth(2)
    }
    .width('100%')
  }
}

开发者可以尝试点击各个变量,并点击Recycle/Reuse按钮查看复用后的重置情况。

reset

需要注意的是,上面的例子中noDecoInfo未被重置,如果存在监听noDecoInfo.age的@Monitor,因为noDecoInfo本身未产生变化,所以该@Monitor也不会被重置,因此在后续第一次更改noDecoInfo.age时,IMonitorValuebefore值将不会被重置,仍是复用前的值。

将上面的例子简化可得下面的例子:

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_Reusablev2]';
const DOMAIN = 0xF811;

@ObservedV2
class Info {
  @Trace public age: number;

  constructor(age: number) {
    this.age = age;
  }
}

@Entry
@ComponentV2
struct Index {
  @Local condition: boolean = true;

  build() {
    Column({ space: 10 }) {
      Button('Recycle/Reuse')
        .width('60%')
        .onClick(() => {
          this.condition = !this.condition;
        })
      if (this.condition) {
        ReusableV2Component()
      }
    }
    .width('100%')
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  noDecoInfo: Info = new Info(30); // 未加装饰器,被视作常量

  @Monitor('noDecoInfo.age')
  onAgeChange(monitor: IMonitor) {
    hilog.info(DOMAIN, TAG, `age change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
  }

  aboutToRecycle() {
    this.noDecoInfo.age = 25;
  }

  aboutToReuse() {
    this.noDecoInfo.age = 35;
  }

  build() {
    Column() {
      Column() {
        Text(`noDecoInfo.age: ${this.noDecoInfo.age}`)
          .onClick(() => {
            this.noDecoInfo.age++;
          }) // 能够触发刷新但是不会被重置
      }
    }
  }
}

建议按照下列步骤进行操作:

  1. 点击noDecoInfo.age: 30,UI刷新为noDecoInfo.age: 31,@Monitor触发并输出日志age change from 30 to 31
  2. 点击Recycle/Reuse两次,UI刷新为noDecoInfo.age: 35,@Monitor触发并输出日志age change from 31 to 35
  3. 点击noDecoInfo.age: 35,UI刷新为noDecoInfo.age: 36,@Monitor触发并输出日志age change from 35 to 36

resetmonitor

由于冻结机制的存在,在aboutToRecycle中赋值不会被@Monitor观察到。而在经历完变量重置后,变量又会被赋予新的值,因此对于组件内状态变量来说,在aboutToRecycle中赋值不会有明显的效果;而常量(例如上面的noDecoInfo)由于冻结机制的存在,在aboutToRecycle中更改age也不会被观察到,并且因为不会被重置,所以相关的@Monitor也不会被重置,即这里的age值本身未被重置,也就不会重置与之绑定的@Monitor。最终表现出来的现象即:第二步回调的@Monitor中,monitor.value()?.before得到的值为31,而非age的初始值30。

针对这一现象,推荐开发者在复用的场景减少使用类似的常量对象包含@Trace属性的写法,以确保复用场景的功能符合预期。

使用场景

在if组件中使用

通过改变if组件的条件可以控制组件回收/复用。

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_Reusablev2]';
const DOMAIN = 0xF811;

@Entry
@ComponentV2
struct Index {
  @Local condition: boolean = true;

  build() {
    Column({ space: 10 }) {
      Button('Recycle/Reuse')
        .width('60%')
        .onClick(() => {
          this.condition = !this.condition;
        }) // 点击切换回收/复用状态
      if (this.condition) {
        ReusableV2Component()
      }
    }
    .width('100%')
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Local message: string = 'Hello World';

  aboutToRecycle() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToRecycle'); // 回收时被调用
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToReuse'); // 复用时被调用
  }

  build() {
    Column() {
      Text(this.message)
    }
  }
}

if

在Repeat组件中使用

Repeat组件懒加载场景中,将会优先使用Repeat组件的缓存池,正常滑动场景、更新场景不涉及组件的回收与复用。当Repeat的缓存池需要扩充时将会向自定义组件要求新的子组件,此时如果复用池中有可复用的节点,将会进行复用。

通过配置Repeat组件VirtualScrollOptions的reusable属性为false,可以关闭Repeat组件自身的复用能力。此时,滑动场景、更新场景均会触发@ReusableV2的回收与复用。

下面的例子中,先点击Change condition会让3个节点进入复用池,而后向下滑动List组件时,可以观察到日志输出ReusableV2Component aboutToReuse,表明Repeat可以使用自定义组件的复用池填充自己的缓存池。

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_Reusablev2]';
const DOMAIN = 0xF811;

@Entry
@ComponentV2
struct Index {
  @Local condition: boolean = true;
  @Local simpleList: number[] = [];

  aboutToAppear(): void {
    for (let i = 0; i < 100; i++) {
      this.simpleList.push(i);
    }
  }

  build() {
    Column() {
      Button('Change condition')
        .onClick(() => {
          this.condition = !this.condition;
        })
      if (this.condition) {
        // 此处仅做演示使用,让复用池中填充3个组件
        ReusableV2Component({ num: 0 })
        ReusableV2Component({ num: 0 })
        ReusableV2Component({ num: 0 })
      }
      List({ space: 10 }) {
        Repeat(this.simpleList)
          .virtualScroll()
          .each((obj: RepeatItem<number>) => {
            ListItem() {
              Column() {
                ReusableV2Component({ num: obj.item })
              }
            }
            .width('100%')
          })
      }.height('50%')
      .cachedCount(2)
    }
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Require @Param num: number;

  aboutToAppear() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToAppear');
  }

  aboutToRecycle() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToRecycle');
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToReuse');
  }

  build() {
    Column() {
      Text(`${this.num}`).fontSize(50)
    }
  }
}

repeat

在Repeat组件非懒加载场景的each属性中使用

Repeat组件非懒加载场景中,会在删除/创建子树时触发回收/复用。

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_Reusablev2]';
const DOMAIN = 0xF811;

@Entry
@ComponentV2
struct Index {
  @Local simpleList: number[] = [1, 2, 3, 4, 5];
  @Local condition: boolean = true;

  build() {
    Column({ space: 10 }) {
      // 点击Button切换condition,删除/创建ReusableV2Component
      Button('Delete/Create Repeat')
        .width('60%')
        .onClick(() => {
          this.condition = !this.condition;
        })
      Button('Add element')
        .width('60%')
        .onClick(() => {
          this.simpleList.push(this.simpleList.length + 1);
        })
      Button('Delete element')
        .width('60%')
        .onClick(() => {
          this.simpleList.pop();
        })
      Button('Change element')
        .width('60%')
        .onClick(() => {
          this.simpleList[0]++;
        })
      if (this.condition) {
        List({ space: 10 }) {
          Repeat(this.simpleList)
            .each((obj: RepeatItem<number>) => {
              ListItem() {
                Column() {
                  ReusableV2Component({ num: obj.item })
                }
                .width('100%')
              }
            })
        }
      }
    }
    .width('100%')
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Require @Param num: number;

  aboutToAppear() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToAppear');
  }

  aboutToRecycle() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToRecycle');
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToReuse');
  }

  build() {
    Column() {
      Text(`${this.num}`)
    }
  }
}

repeat-nonvirtual

在ForEach组件中使用

说明:

推荐开发者使用Repeat组件的非懒加载场景代替ForEach组件。

下面的例子中使用了ForEach组件渲染了数个可复用组件,由于每次点击Click to change按钮时key值都会发生变化,因此从第二次点击开始都会触发回收与复用(由于ForEach先判断有无可复用节点时复用池仍未初始化,因此第一次点击会创建新的节点,而后初始化复用池同时回收节点)。

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_Reusablev2]';
const DOMAIN = 0xF811;

@Entry
@ComponentV2
struct Index {
  @Local simpleList: number[] = [0, 1, 2, 3, 4, 5];

  build() {
    Column() {
      ForEach(this.simpleList, (num: number, index) => {
        Row() {
          Button('Click to change')
            .margin({ right: 10 })
            .onClick(() => {
              this.simpleList[index]++;
            })
          ReusableV2Component({ num: num })
        }
        .margin({ bottom: 10 })
      }) // 每次修改完key发生变化
    }
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Require @Param num: number;

  aboutToAppear() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToAppear', this.num); // 创建时触发
  }

  aboutToRecycle() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToRecycle', this.num); // 回收时触发
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, 'ReusableV2Component aboutToReuse', this.num); // 复用时触发
  }

  build() {
    Column() {
      Text(`child: ${this.num}`)
    }
  }
}

foreach

在LazyForEach组件中使用

说明:

推荐开发者使用Repeat组件的懒加载场景代替LazyForEach组件。

下面的例子中使用了LazyForEach渲染了数个可复用组件,在滑动时可以先观察到组件创建,直到预加载节点全部创建完成之后,再滑动则触发复用和回收。

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_Reusablev2]';
const DOMAIN = 0xF811;

class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: StringData[] = [];

  public totalCount(): number {
    return 0;
  }

  public getData(index: number): StringData {
    return this.originDataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      hilog.info(DOMAIN, TAG, 'add listener');
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      hilog.info(DOMAIN, TAG, 'remove listener');
      this.listeners.splice(pos, 1);
    }
  }

  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    });
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    });
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);
    });
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    });
  }

  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
    });
  }

  notifyDatasetChange(operations: DataOperation[]): void {
    this.listeners.forEach(listener => {
      listener.onDatasetChange(operations);
    });
  }
}

class MyDataSource extends BasicDataSource {
  private dataArray: StringData[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): StringData {
    return this.dataArray[index];
  }

  public addData(index: number, data: StringData): void {
    this.dataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  public pushData(data: StringData): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

@ObservedV2
class StringData {
  @Trace message: string;

  constructor(message: string) {
    this.message = message;
  }
}

@Entry
@ComponentV2
struct Index {
  data: MyDataSource = new MyDataSource(); // 数据源

  aboutToAppear() {
    for (let i = 0; i <= 200; i++) {
      this.data.pushData(new StringData('Hello' + i));
    }
  }

  build() {
    List({ space: 3 }) {
      LazyForEach(this.data, (item: StringData, index: number) => {
        ListItem() {
          Column() {
            Text(item.message)
            ChildComponent({ data: item.message })
              .onClick(() => {
                item.message += '!'; // message为@Trace装饰的变量,可观察变化
              })
          }
        }
      })
    }.cachedCount(5)
  }
}

@ReusableV2
@ComponentV2
struct ChildComponent {
  @Param @Require data: string;

  aboutToAppear(): void {
    hilog.info(DOMAIN, TAG, 'ChildComponent aboutToAppear', this.data);
  }

  aboutToDisappear(): void {
    hilog.info(DOMAIN, TAG, 'ChildComponent aboutToDisappear', this.data);
  }

  aboutToReuse(): void {
    hilog.info(DOMAIN, TAG, 'ChildComponent aboutToReuse', this.data); // 复用时触发
  }

  aboutToRecycle(): void {
    hilog.info(DOMAIN, TAG, 'ChildComponent aboutToRecycle', this.data); // 回收时触发
  }

  build() {
    Row() {
      Text(this.data).fontSize(50)
    }
  }
}

lazyforeach