大家在开发 HarmonyOS 应用时,是不是总遇到组件反复创建销毁导致性能下降的问题?别担心,今天咱们就来聊聊能解决这个问题的 @ReusableV2 装饰器。它可是个好东西,能让组件复用变得轻松高效,下面咱们就详细说说它的方方面面。

一、@ReusableV2 是啥?一句话带你了解

简单来说,@ReusableV2 就是给 V2 自定义组件(也就是用 @ComponentV2 装饰的组件)加上复用能力的装饰器。有了它,组件不用每次都销毁再重建,能直接回收复用,大大降低性能开销。

不过它和之前的 @Reusable 有几个明显不同:

  • aboutToReuse 这个生命周期函数没有入参
  • 回收和复用的时候,会递归调用所有子组件的相关回调,不管子组件能不能复用
  • 组件回收时会冻结,期间不能刷新 UI、触发 @Monitor 回调,而且解冻后也不会补刷新
  • 复用时会自动重置组件内的状态变量、重新计算 @Computed 和相关 @Monitor

咱们先看个最基本的用法代码:

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Local message: string = 'Hello World';
  build () {
      Column() {
        Text(this.message)
      }
  }
}

二、装饰器本身有啥讲究?

@ReusableV2 这装饰器用法不复杂,但有几点得记牢:

  • 只能装饰用 @ComponentV2 装饰的 V2 自定义组件
  • 只能把它装饰的组件当作 V2 自定义组件的子组件用
  • 不用传参数,直接加在组件前面就行

给大家看个正确的装饰示例:

@ReusableV2
@ComponentV2
struct MyReusableComponent {
  build() {
    Column() {
      Text("我是可复用组件")
    }
  }
}

三、接口说明:reuseId 很重要

要实现组件复用,reuseId 是关键,相同 reuseId 的组件才能互相复用。这里涉及到几个相关的接口:

  • ReuseIdCallback:就是个返回字符串的函数,用来计算 reuseId
  • ReuseOptions:保存 reuseId 信息的对象,里面就一个 reuseId 属性,值是 ReuseIdCallback
  • reuse 属性:给组件指定 reuseId 的属性,参数是 ReuseOptions

咱们看个例子,了解下 reuseId 的各种设置方式:

@Entry
@ComponentV2
struct Index {
  build() {
    Column() {
      // 明确指定reuseId为'reuseComponent'
      ReusableV2Component()
        .reuse({reuseId: () => 'reuseComponent'})
      // 用空字符串,默认会用组件名'ReusableV2Component'当reuseId
      ReusableV2Component()
        .reuse({reuseId: () => ''})
      // 不指定reuseId,也默认用组件名
      ReusableV2Component()
    }
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  build() {
    Text("我是可复用组件")
  }
}

四、使用限制:这些坑可别踩

虽然 @ReusableV2 很好用,但也有不少限制,不注意就容易出问题:

  1. 只能在 V2 自定义组件里用。如果在 V1 组件(@Component 装饰的)里用,轻则编译报错,复杂场景下可能运行时才报错。

// 正确用法:在V2组件里用
@Entry
@ComponentV2
struct Index {
  build() {
    Column() {
      ReusableV2Component() // 没问题
    }
  }
}

// 错误用法:在V1组件里用
@Component
struct V1Component {
  build() {
    Column() {
      ReusableV2Component() // 编译就报错
    }
  }
}

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

  1. V1 和 V2 的复用组件混用有规矩。简单说就是:

  • V1 普通组件能包含 V1 普通、V2 普通、V1 复用组件,但不能包含 V2 复用组件
  • V2 普通组件能包含 V1 普通、V2 普通、V2 复用组件,但不能包含 V1 复用组件
  • V1 复用组件能包含 V1 普通、V2 普通、V1 复用组件,不能包含 V2 复用组件
  • V2 复用组件能包含 V1 普通、V2 普通、V2 复用组件,不能包含 V1 复用组件

  1. 不能直接在 Repeat 的 template 里用 V2 复用组件,得嵌套在普通 V2 组件里才行。

@Entry
@ComponentV2
struct Index {
  @Local arr: number[] = [1, 2, 3];
  build() {
    Column() {
      List() {
        Repeat(this.arr)
          .template('a', (ri) => {
            ListItem() {
              // 直接用不行,编译报错
              // ReusableV2Component({ val: ri.item})
              
              // 得像这样,用普通V2组件包一层
              NormalV2Component({ val: ri.item })
            }
          })
      }
    }
  }
}

// 普通V2组件
@ComponentV2
struct NormalV2Component {
  @Require @Param val: number;
  build() {
    ReusableV2Component({ val: this.val }) // 这样就没问题
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Require @Param val: number;
  build() {
    Text(`val: ${this.val}`)
  }
}

五、回收与复用的生命周期:组件的 "轮回"

@ReusableV2 给组件加了两个特殊的生命周期函数,让我们能在回收和复用时做些操作:

  • aboutToRecycle:组件被回收时调用
  • aboutToReuse:组件被复用时调用

咱们用 if 组件的例子看看它们是怎么工作的:

@Entry
@ComponentV2
struct Index {
  @Local condition1: boolean = false;
  @Local condition2: boolean = true;
  
  build() {
    Column() {
      Button('显示组件').onClick(() => {this.condition1 = true;})
      Button('回收组件').onClick(() => {this.condition2 = false;})
      Button('复用组件').onClick(() => {this.condition2 = true;})
      Button('销毁组件').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 () {
    console.log('组件创建了');
  }
  aboutToDisappear () {
    console.log('组件销毁了');
  }
  aboutToRecycle () {
    console.log('组件要被回收了');
  }
  aboutToReuse () {
    console.log('组件要被复用了');
  }
  build() {
    Text('我是可复用组件')
  }
}

操作步骤和对应的日志:

  1. 点 "显示组件":创建组件,输出 "组件创建了"
  2. 点 "回收组件":组件被回收,输出 "组件要被回收了"
  3. 点 "复用组件":组件被复用,输出 "组件要被复用了"
  4. 点 "销毁组件":组件被销毁,输出 "组件销毁了"

六、复用阶段的冻结:组件也会 "休眠"

V2 复用组件被回收时会进入冻结状态,这时候不管怎么改变量,都不会触发 UI 刷新和 @Monitor 回调。直到复用的时候,解除冻结,之后的修改才能正常生效。

看个例子感受下:

@ObservedV2
class Info {
  @Trace age: number = 25;
}
const info: Info = new Info();

@Entry
@ComponentV2
struct Index {
  @Local condition: boolean = true;
  build() {
    Column() {
      Button('切换复用/回收').onClick(()=>{this.condition=!this.condition;})
      Button('增加年龄').onClick(()=>{info.age++;})
      if (this.condition) {
        ReusableV2Component()
      }
    }
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Local info: Info = info;
  @Monitor('info.age')
  onAgeChange () {
    console.log('年龄变了');
  }
  aboutToRecycle () {
    console.log('要回收了');
    this.info.age++; // 这里改了也不会触发刷新和Monitor
  }
  aboutToReuse () {
    console.log('要复用了');
    this.info.age++; // 这里改了会触发
  }
  build() {
    Column() {
      Text(`年龄:${this.info.age}`)
    }
  }
}

操作后能看到:

  • 没回收的时候,点 "增加年龄",UI 会变,也会输出 "年龄变了"
  • 点 "切换" 进入回收状态,再点 "增加年龄",UI 不变,也不输出日志
  • 再点 "切换" 进入复用状态,UI 会更新,输出 "年龄变了"

七、复用前的状态变量重置:回到 "初始状态"

组件被复用时,里面的状态变量会自动重置,不同装饰器的变量重置规则不一样:

  • @Local:用定义时的初始值
  • @Param:有外部传入就用传入的,没有就用本地初始值(@Once 的也会重置)
  • @Event:有外部传入就用传入的,没有就用本地初始值,没初始值就用空实现
  • @Provider:用定义时的初始值
  • @Consumer:有对应的 @Provider 就用它的值,没有就用本地初始值
  • @Computed:重新计算
  • @Monitor:重置后触发
  • 常量(包括 readonly 和没装饰器的):不重置

看个综合例子:

@ObservedV2
class Info {
  @Trace age: number;
  constructor(age: number) {
    this.age = age;
  }
}

@Entry
@ComponentV2
struct Index {
  @Local localNum: number = 0;
  @Provider('parentPro') parentPro: number = 100;
  @Local condition: boolean = true;
  
  build() {
    Column() {
      Button('切换回收/复用').onClick(()=>{this.condition=!this.condition;})
      if (this.condition) {
        ReusableV2Component({
          outParam: this.localNum,
          onceParam: this.localNum
        })
      }
    }
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Local localVal: number = 0; // 重置为0
  @Local info: Info = new Info(25); // 重置为25
  @Param inParam: number = 1; // 外部没传,重置为1
  @Require @Param outParam: number; // 重置为外部传入的最新值
  @Require @Param @Once onceParam: number; // 重置为外部传入的最新值
  @Provider('selfPro') selfPro: number = 0; // 重置为0
  @Consumer('parentPro') parentCon: number = 0; // 重置为parentPro的最新值
  noDecoVar: number = 0; // 不重置
  readonly readOnlyVar: number = 0; // 不重置
  
  build() {
    Column() {
      Text(`localVal: ${this.localVal}`).onClick(()=>{this.localVal++;})
      Text(`info.age: ${this.info.age}`).onClick(()=>{this.info.age++;})
      Text(`outParam: ${this.outParam}`)
    }
  }
}

大家可以自己试试,改改这些变量的值,再切换回收 / 复用,看看是不是按照上面说的规则重置的。

八、使用场景:这些地方用它准没错

@ReusableV2 在很多场景都能派上用场,咱们逐个看看:

  1. 在 if 组件中使用
    通过改变 if 的条件来控制组件的回收和复用,前面的生命周期例子其实就是这个场景,很简单实用。

  2. 在 Repeat 组件(懒加载)中使用
    Repeat 懒加载时,滑动列表会优先用缓存池里的组件,如果缓存不够,就会从复用池里取。

@Entry
@ComponentV2
struct Index {
  @Local list: number[] = [];
  aboutToAppear() {
    for (let i = 0; i < 100; i++) {
      this.list.push(i)
    }
  }
  build() {
    Column() {
      List() {
        Repeat(this.list)
          .virtualScroll()
          .each((obj) => {
            ListItem() {
              ReusableV2Component({ num: obj.item })
            }
          })
      }.height('50%')
    }
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Require @Param num: number;
  aboutToRecycle () {
    console.log(`回收了 ${this.num}`);
  }
  aboutToReuse () {
    console.log(`复用了 ${this.num}`);
  }
  build() {
    Text(`${this.num}`).fontSize(30)
  }
}

滑动列表的时候,就能看到控制台输出回收和复用的日志了。

  1. 在 Repeat 组件(非懒加载)中使用
    非懒加载时,增删列表元素会触发组件的回收和复用。

@Entry
@ComponentV2
struct Index {
  @Local list: number[] = [1, 2, 3, 4, 5];
  build() {
    Column() {
      Button('增加元素').onClick(()=>{this.list.push(this.list.length+1);})
      Button('删除元素').onClick(()=>{this.list.pop();})
      List() {
        Repeat(this.list)
          .each((obj) => {
            ListItem() {
              ReusableV2Component({ num: obj.item })
            }
          })
      }
    }
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Require @Param num: number;
  aboutToRecycle () {
    console.log(`回收 ${this.num}`);
  }
  aboutToReuse () {
    console.log(`复用 ${this.num}`);
  }
  build() {
    Text(`${this.num}`)
  }
}

点增加或删除按钮,就能看到对应的回收和复用日志。

  1. 在 ForEach 组件中使用
    虽然官方推荐用 Repeat 代替 ForEach,但如果一定要用 ForEach,也可以用 @ReusableV2。

@Entry
@ComponentV2
struct Index {
  @Local list: number[] = [0, 1, 2, 3, 4];
  build() {
    Column() {
      ForEach(this.list, (num, index) => {
        Row() {
          Button('修改').onClick(()=>{this.list[index]++;})
          ReusableV2Component({ num: num })
        }
      })
    }
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Require @Param num: number;
  aboutToRecycle () {
    console.log(`回收 ${this.num}`);
  }
  aboutToReuse () {
    console.log(`复用 ${this.num}`);
  }
  build() {
    Text(`数字:${this.num}`)
  }
}

点修改按钮,就能触发组件的回收和复用了。

  1. 在 LazyForEach 组件中使用
    和 Repeat 懒加载类似,滑动列表时会复用组件,不过官方更推荐用 Repeat 的懒加载。

// 数据源相关代码
class DataSource {
  private data: string[] = [];
  constructor() {
    for (let i = 0; i <= 200; i++) {
      this.data.push('item' + i);
    }
  }
  totalCount() { return this.data.length; }
  getData(index: number) { return this.data[index]; }
}

@Entry
@ComponentV2
struct Index {
  data: DataSource = new DataSource();
  build() {
    List() {
      LazyForEach(this.data, (item) => {
        ListItem() {
          ReusableV2Component({ text: item })
        }
      })
    }.cachedCount(5)
  }
}

@ReusableV2
@ComponentV2
struct ReusableV2Component {
  @Require @Param text: string;
  aboutToRecycle () {
    console.log(`回收 ${this.text}`);
  }
  aboutToReuse () {
    console.log(`复用 ${this.text}`);
  }
  build() {
    Text(this.text).fontSize(20)
  }
}

滑动列表,当超出缓存范围后,就会触发组件的回收和复用。

总结一下

@ReusableV2 装饰器真的是个提升性能的好帮手,能让组件在该回收的时候回收,该复用的时候复用,大大减少了创建销毁组件的开销。不过它的使用规则和限制也不少,比如只能在 V2 组件里用、变量重置有特定规则、冻结状态的特性等等,这些都需要咱们在开发中特别注意。

只要把这些知识点都掌握了,合理运用 @ReusableV2,相信大家的 HarmonyOS 应用性能肯定能更上一层楼!如果还有啥不清楚的,回头再看看这些例子,多动手试试,肯定就能熟练掌握啦。

Logo

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

更多推荐