为了降低反复创建销毁自定义组件带来的性能开销,开发者可以使用@ReusableV2装饰@ComponentV2装饰的自定义组件,达成组件复用的效果。
在阅读本文前,建议提前阅读:@Reusable装饰器:V1组件复用。
说明
从API version 18开始,可以使用@ReusableV2装饰@ComponentV2装饰的自定义组件。
从API version 18开始,该装饰器支持在元服务中使用。
概述
@ReusableV2用于装饰V2的自定义组件,表明该自定义组件具有被复用的能力:
- @ReusableV2仅能装饰V2的自定义组件,即@ComponentV2装饰的自定义组件。并且仅能将@ReusableV2装饰的自定义组件作为V2自定义组件的子组件使用。
- @ReusableV2同样提供了aboutToRecycle和aboutToReuse的生命周期,在组件被回收时调用aboutToRecycle,在组件被复用时调用aboutToReuse,但与@Reusable不同的是,aboutToReuse没有入参。
- 在回收阶段,会递归地调用所有子组件的aboutToRecycle回调(即使子组件未被标记可复用);在复用阶段,会递归地调用所有子组件的aboutToReuse回调(即使子组件未被标记可复用)。
- @ReusableV2装饰的自定义组件会在被回收期间保持冻结状态,即无法触发UI刷新、无法触发@Monitor回调,与freezeWhenInactive标记位不同的是,在解除冻结状态后,不会触发延后的刷新。
- @ReusableV2装饰的自定义组件会在复用时自动重置组件内状态变量的值、重新计算组件内@Computed以及与之相关的@Monitor。不建议开发者在aboutToRecycle中更改组件内状态变量,详见复用前的组件内状态变量重置。
- V1和V2的复用组件可在一定规则下混用,详见使用限制第二点。
- 不建议开发者嵌套滥用@ReusableV2装饰器,这可能会导致复用效率降低以及内存开销变大。
装饰器说明
| @ReusableV2装饰器 | 说明 |
|---|---|
| 装饰器参数 | 无 |
| 可装饰的组件 | @ComponentV2装饰的自定义组件 |
| 装饰器作用 | 表明该组件可被复用 |
接口说明
reuse、ReuseOptions、ReuseIdCallback的接口说明参考API文档:复用选项。
TypeScript
@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的复用组件将导致编译期报错,编译期无法校验到的复杂场景下将会有运行时报错。
-
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自定义组件中。
TypeScript
@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。
TypeScript
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')
}
}
}
建议按下面顺序进行操作:
- 点击step1. appear,此时condition1变为true,Index中的if组件切换分支,创建出NormalV2Component,由于condition2初始值为true,所以NormalV2Component中的if条件满足,尝试创建ReusableV2Component。此时复用池中无元素,因此会创建ReusableV2Component,并回调aboutToAppear的方法,输出ReusableV2Component aboutToAppear called的日志。
- 点击step2. recycle,此时condition2变为false,通过@Param同步给NormalV2Component,if条件切换,由于ReusableV2Component使用了@ReusableV2,因此会将该组件回收至复用池而不是销毁,回调aboutToRecycle的方法并输出ReusableV2Component aboutToRecycle called的日志。
- 点击step3. reuse,此时condition2变为true,通过@Param传递给NormalV2Component,if条件切换,由于ReusableV2Component使用了@ReusableV2,因此在创建该组件时尝试去复用池中寻找。此时复用池中有第二步放入的组件实例,因此从复用池中取出复用,回调aboutToReuse方法并输出ReusableV2Component aboutToReuse called的日志。
- 点击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的调用。
在复杂的混用场景中,是否冻结的规则可以总结为以下两点:
- V1的组件根据是否开启组件冻结freezeWhenInactive决定。
- V2的组件自动被冻结。
复用前的组件内状态变量重置
与@Reusable不同的是,@ReusableV2在复用前会重置组件中的状态变量以及相关的@Computed、@Monitor的内容。在复用的过程当中,所有的V2自定义组件,无论是否被标记了@ReusableV2,都会经历这一个重置过程。
重置会按照变量在组件中定义的顺序按照下面的规则依次进行:
展开
| 装饰器 | 重置方法 |
|---|---|
| @Local | 直接使用定义时的初始值重新赋值。 |
| @Param | 如果有外部传入则使用外部传入值重新赋值,否则用本地初始值重新赋值。注意:@Once装饰的变量同样会被重置初始化一次。 |
| @Event | 如果有外部传入则使用外部传入值重新赋值,否则用本地初始值重新赋值。如果本地没有初始值,则生成默认的空实现。 |
| @Provider | 直接使用定义时的初始值重新赋值。 |
| @Consumer | 如果有对应的@Provider则直接使用@Provider对应的值,否则使用本地初始值重新赋值。 |
| @Computed | 使用当前最新的值重新计算一次,如果使用到的变量还未被重置,将会使用重置前的值,因此推荐开发者将@Computed定义在所使用的变量之后。 |
| @Monitor | 在上述所有变量重置完成之后触发。重置过程中产生的变量变化不会触发@Monitor回调,仅更新IMonitorValue中的before值。重置过程中不产生变化的赋值不会触发@Monitor的重置。 |
| 常量 | 包括readonly的常量,不重置。 |
使用场景
这一章没看到实际使用价值,之后再整理使用场景及后续部分。