为了降低反复创建销毁自定义组件带来的性能开销,开发者可以使用@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装饰的自定义组件
装饰器作用 表明该组件可被复用

接口说明

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装饰器:组件复用

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

    • 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复用组件 支持 支持 支持 不支持,编译报错
      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() {
                    ReusableV2Component({ val: ri.item}) // 暂不支持,编译期报错
                    ReusableV2Builder(ri.item) // 暂不支持,运行时报错
                    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。

    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(){
          Button('step1. appear')
            .onClick(() => {
              this.condition1 = true;
            })
          Button('step2. recycle')
            .onClick(() => {
              this.condition2 = false;
            })
          Button('step3. reuse')
            .onClick(() => {
              this.condition2 = true;
            })
          Button('step4. disappear')
            .onClick(() => {
              this.condition1 = false;
            })
          if (this.condition1) {
            NormalV2Component({ condition: this.condition2 })
          }
        }
      }
    }
    @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(与子组件是否被标记复用无关),直到遍历完所有的孩子组件。

    复用阶段的冻结

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

    使用场景

    这一章没看到实际使用价值,之后再整理使用场景及后续部分。

    Logo

    讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

    更多推荐