HarmonyOS 中的 @ReusableV2 装饰器:让组件复用更简单
《HarmonyOS组件复用利器:@ReusableV2装饰器详解》 摘要:@ReusableV2是HarmonyOS中提升组件性能的关键装饰器,专为V2自定义组件设计。它通过组件复用机制显著降低性能开销,与旧版@Reusable相比具有更精细的生命周期控制。文章详细解析了其核心特性:通过reuseId实现组件匹配、严格的使用限制条件、独特的冻结状态机制,以及复用时的变量重置规则。
大家在开发 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 很好用,但也有不少限制,不注意就容易出问题:
- 只能在 V2 自定义组件里用。如果在 V1 组件(@Component 装饰的)里用,轻则编译报错,复杂场景下可能运行时才报错。
// 正确用法:在V2组件里用
@Entry
@ComponentV2
struct Index {
build() {
Column() {
ReusableV2Component() // 没问题
}
}
}
// 错误用法:在V1组件里用
@Component
struct V1Component {
build() {
Column() {
ReusableV2Component() // 编译就报错
}
}
}
@ReusableV2
@ComponentV2
struct ReusableV2Component {
build() {}
}
- V1 和 V2 的复用组件混用有规矩。简单说就是:
- V1 普通组件能包含 V1 普通、V2 普通、V1 复用组件,但不能包含 V2 复用组件
- V2 普通组件能包含 V1 普通、V2 普通、V2 复用组件,但不能包含 V1 复用组件
- V1 复用组件能包含 V1 普通、V2 普通、V1 复用组件,不能包含 V2 复用组件
- V2 复用组件能包含 V1 普通、V2 普通、V2 复用组件,不能包含 V1 复用组件
- 不能直接在 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('我是可复用组件')
}
}
操作步骤和对应的日志:
- 点 "显示组件":创建组件,输出 "组件创建了"
- 点 "回收组件":组件被回收,输出 "组件要被回收了"
- 点 "复用组件":组件被复用,输出 "组件要被复用了"
- 点 "销毁组件":组件被销毁,输出 "组件销毁了"
六、复用阶段的冻结:组件也会 "休眠"
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 在很多场景都能派上用场,咱们逐个看看:
-
在 if 组件中使用
通过改变 if 的条件来控制组件的回收和复用,前面的生命周期例子其实就是这个场景,很简单实用。 -
在 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)
}
}
滑动列表的时候,就能看到控制台输出回收和复用的日志了。
- 在 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}`)
}
}
点增加或删除按钮,就能看到对应的回收和复用日志。
- 在 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}`)
}
}
点修改按钮,就能触发组件的回收和复用了。
- 在 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 应用性能肯定能更上一层楼!如果还有啥不清楚的,回头再看看这些例子,多动手试试,肯定就能熟练掌握啦。
更多推荐



所有评论(0)