状态管理存在V1和V2版本,那在实际开发过程中,状态管理是推荐用V1开发还是V2开发?
ArkUI提供了状态管理(V1)和状态管理(V2)用于应用开发中状态变量的管理。状态管理V1使用代理观察数据,当创建一个状态变量时,同时也创建了一个数据代理观察者。状态管理V2将观察能力增强到数据本身,数据本身就是可观察的,更改数据会触发相应的视图的更新。两种状态管理方式存在的一些差异,可参考:状态管理V1与V2能力对比、状态管理V1现状以及V2优点、V1状态变量更新和V2状态变量更新差异。
针对应用开发过程中常见的场景,汇总如下表:
场景 | 状态管理选择说明 |
|---|---|
深度状态观测 | 为了对嵌套类对象属性直接观测,推荐使用V2。 |
计算属性重复计算 | 为了避免重用属性而重复计算导致的性能问题,推荐使用V2。 |
状态变量修改监听 | 为了对状态变量具有深度监听的能力,且能获取到变化前的值,推荐使用V2。 |
使用animateTo实现动画 | 由于在状态管理V2中使用animateTo可能出现动画效果异常,推荐使用V1,API22以后可以通过applySync/flushUpdates/flushUIUpdates接口:同步刷新辅助方法规避。 |
使用高级组件 | 由于在低版本API中状态管理V2中未提供部分高级组件,推荐使用V1。 从API version 18开始,系统预置UI组件支持在状态管理V2组件中使用。 |
自定义局部页面主题 | 由于在低版本API中状态管理V2中未提供WithTheme组件,推荐使用V1。从API version 18开始,该接口支持在状态管理V2组件中使用。 |
自定义组件复用 | 由于在低版本API中状态管理V2中未提供@Reusable装饰器,推荐使用V1。从API version 18开始,该装饰器支持在状态管理V2组件中使用。 |
一、状态管理V2的优势与适用场景。
@Observed
class Father {
son: Son;
constructor(name: string, age: number) {
this.son = new Son(name, age);
}
}
@Observed
class Son {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
@Component
struct Child {
// 使用@ObjectLink装饰器与自定义组件来实现观测
@ObjectLink son: Son;
build() {
Row() {
Column() {
Text(`name: ${this.son.name} age: ${this.son.age}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.son.age++;
});
}
.width('100%');
}
.height('100%');
}
}
@Component
export struct NestedV1 {
@State father: Father = new Father('John', 8);
build() {
Column() {
Text('使用V1监控嵌套类对象属性的变化')
.fontSize(20)
.fontWeight(FontWeight.Bold);
Child({ son: this.father.son });
}
.width('100%')
.height('100%');
}
} @ObservedV2
class Son {
// 在嵌套类中使用@Trace装饰的属性具有被观测变化的能力
@Trace age: number = 100;
name: string;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
class Father {
son: Son;
constructor(name: string, age: number) {
this.son = new Son(name, age);
}
}
@ComponentV2
export struct NestedV2 {
father: Father = new Father('John', 8);
build() {
Column() {
Text('使用V2监控嵌套类对象属性的变化')
.fontSize(20)
.fontWeight(FontWeight.Bold);
Row() {
Column() {
Text(`name: ${this.father.son.name} age: ${this.father.son.age}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.father.son.age++;
});
}
.width('100%');
}
.height('100%');
}
.height('100%')
.width('100%')
.height('100%');
}
} 总结:通过V1这种方式虽然能够实现对嵌套类中属性变化的观测,但是当嵌套层级较深时,代码将会变得十分复杂,易用性差。采用V2的话,使用@ObservedV2装饰的嵌套类中被@Trace装饰的属性具有被观测变化的能力,当该属性值变化时,会触发该属性绑定的UI组件刷新,代码较简洁。
@Component
export struct CalculationV1 {
firstName: string = 'Li';
@State lastName: string = 'Hua';
build() {
Column() {
Text('使用V1重用属性导致重复计算')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 50 });
// 两个Text组件UI重复计算
Text(this.firstName + ' ' + this.lastName)
.margin({ bottom: 8 });
Text(this.firstName + ' ' + this.lastName);
Button('changed lastName')
.margin({ top: 16 })
.onClick(() => {
this.lastName += 'a';
});
}
.width('100%')
.height('100%');
}
} @ComponentV2
export struct CalculationV2 {
@Local firstName: string = 'Li';
@Local lastName: string = 'Hua';
@Computed
get fullName() {
console.info('Computed');
return this.firstName + ' ' + this.lastName;
}
build() {
Column() {
Text('使用V2计算属性解决重复计算')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 50 });
// this.fullName被绑定在两个Text组件上,计算只发生了一次
Text(this.fullName)
.margin({ bottom: 8 });
Text(this.fullName);
Button('changed lastName')
.margin({ top: 16 })
.onClick(() => {
this.lastName += 'a';
});
}
.width('100%')
.height('100%');
}
} 总结:通过V1这种方式,对于前两个Text组件,this.lastName + ' '+ this.firstName这段逻辑被求解了两次。如果UI中有多处重复计算,会产生较大的性能开销。采用V2的话,this.fullName被绑定在两个Text组件上,计算只发生了一次,解决UI多次重用该属性从而重复计算导致的性能问题。
@Observed
class Info {
name: string = 'Tom';
age: number = 25;
}
@Component
export struct ModificationV1 {
@State @Watch('onInfoChange') info: Info = new Info();
// 更改info中的name或age属性时,均会触发info注册的@Watch回调
onInfoChange() {
console.info(`info after change name: ${this.info.name}, age: ${this.info.age} `);
}
build() {
Column() {
Text('使用V1中@Watch监听状态变量修改')
.fontSize(20)
.fontWeight(FontWeight.Bold);
Row() {
Column() {
Text(`name: ${this.info.name}, age: ${this.info.age}`);
Button('change info name')
.margin({ top: 16 })
.onClick(() => {
this.info.name = 'Jack';
});
Button('change info age')
.margin({ top: 16 })
.onClick(() => {
this.info.age = 30;
});
}
.width('100%');
}
.height('100%');
}
.width('100%')
.height('100%');
}
} @ObservedV2
class Info {
@Trace name: string = 'Tom';
@Trace age: number = 25;
@Monitor('name')
// 更改name属性时触发此回调
onNameChange(monitor: IMonitor) {
console.info(`name change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
}
@Monitor('age')
// 更改age属性时触发此回调
onAgeChange(monitor: IMonitor) {
console.info(`age change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
}
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
@ComponentV2
export struct ModificationV2 {
@Local info: Info = new Info('Tom', 25);
build() {
Column() {
Text('使用V2中@Monitor监听状态变量修改')
.fontSize(20)
.fontWeight(FontWeight.Bold);
Row() {
Column() {
Text(`name: ${this.info.name}, age: ${this.info.age}`);
Button('change info name')
.margin({ top: 16 })
.onClick(() => {
this.info.name = 'Jack';
});
Button('change info age')
.margin({ top: 16 })
.onClick(() => {
this.info.age = 30;
});
}
.width('100%');
}
.height('100%');
}
.width('100%')
.height('100%');
}
} 总结:通过V1这种方式,更改info中的name或age属性时,均会触发info注册的@Watch回调。在这个回调中,由于无法获取数据更改前的值,在业务逻辑更加复杂的场景下,无法准确知道是哪一个属性或元素发生了变化而触发了@Watch事件,这不便于开发者对变量的更改进行准确监听。而对于通过V2的实现方式,准确监听了对象中每个属性的变化及其变化前后的值。
@Component
export struct DecoratorV1 {
@State title: string = 'Title One';
@State fontColor: Color = Color.Black;
build() {
Column() {
Text('使用V1在父子组件之间同步数据')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 50 });
Child({
title: this.title,
fontColor: this.fontColor
});
}
.width('100%')
.height('100%');
}
}
@Component
struct Child {
// @Link实现了父子组件双向数据绑定,子组件可以直接修改父组件的状态,可能会导致状态流向不清晰
@Link title: string;
@Prop fontColor: Color = Color.Red;
build() {
Column() {
Text(`${this.title}`)
.fontColor(this.fontColor);
Button('change to Title Two')
.margin({ top: 16 })
.onClick(() => {
this.changeFactory(2);
});
Button('change to Title One')
.margin({ top: 16 })
.onClick(() => {
this.changeFactory(1);
});
};
}
changeFactory(type: number) {
if (type == 1) {
this.title = 'Title One';
} else if (type == 2) {
this.title = 'Title Two';
}
}
} @ComponentV2
export struct DecoratorV2 {
@Local title: string = 'Title One';
@Local fontColor: Color = Color.Black;
build() {
Column() {
Text('使用V2在父子组件之间同步数据')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 50 });
Child({
title: this.title,
fontColor: this.fontColor,
changeFactory: (type: number) => {
if (type == 1) {
this.title = 'Title One';
} else if (type == 2) {
this.title = 'Title Two';
}
}
});
}
.width('100%')
.height('100%');
}
}
@ComponentV2
struct Child {
// @Param用于单向数据传递,子组件无法直接修改父组件的状态
@Param title: string = '';
@Param fontColor: Color = Color.Red;
// 实现父子组件双向同步的能力,通过事件驱动的方式实现,状态流向更加明确
@Event changeFactory: (x: number) => void = () => {
};
build() {
Column() {
Text(`${this.title}`)
.fontColor(this.fontColor);
Button('change to Title Two')
.margin({ top: 16 })
.onClick(() => {
this.changeFactory(2);
});
Button('change to Title One')
.margin({ top: 16 })
.onClick(() => {
this.changeFactory(1);
});
};
}
} 总结:V1通过@Link实现了父子组件双向数据绑定,子组件可以直接修改父组件的状态,无需额外的事件处理,但可能会导致状态流向不清晰,尤其是在复杂组件中,维护成本较高。而V2使用@Param用于单向数据传递,子组件无法直接修改父组件的状态。为了实现父子组件双向同步的能力,通过事件驱动的方式实现,状态流向更加明确,代码结构更加清晰,维护成本较低。
完整示例:以上代码以自定义组件形式出现,想要成功运行,可参考以下入口示例测试。
import { NestedV1 } from '../views/Scene1/NestedV1';
import { NestedV2 } from '../views/Scene1/NestedV2';
import { CalculationV1 } from '../views/Scene2/CalculationV1';
import { CalculationV2 } from '../views/Scene2/CalculationV2';
import { ModificationV1 } from '../views/Scene3/ModificationV1';
import { ModificationV2 } from '../views/Scene3/ModificationV2';
import { DecoratorV1 } from '../views/Scene4/DecoratorV1';
import { DecoratorV2 } from '../views/Scene4/DecoratorV2';
@Entry
@Component
struct MainPage {
@State currentIndexOut: number = 0;
@State currentIndexIn1: number = 0;
@State currentIndexIn2: number = 0;
@State currentIndexIn3: number = 0;
@State currentIndexIn4: number = 0;
build() {
Tabs({ index: this.currentIndexOut }) {
TabContent() {
Tabs() {
TabContent() {
NestedV1();
}.tabBar('V1');
TabContent() {
NestedV2();
}.tabBar('V2');
}
.height('100%')
.width('100%')
.scrollable(false)
.barMode(BarMode.Fixed)
.barBackgroundColor('#ffffff')
.barPosition(BarPosition.Start)
.onChange((index: number) => {
this.currentIndexIn1 = index;
});
}.tabBar('场景一');
TabContent() {
Tabs() {
TabContent() {
CalculationV1();
}.tabBar('V1');
TabContent() {
CalculationV2();
}.tabBar('V2');
}
.height('100%')
.width('100%')
.scrollable(false)
.barMode(BarMode.Fixed)
.barBackgroundColor('#ffffff')
.barPosition(BarPosition.Start)
.onChange((index: number) => {
this.currentIndexIn2 = index;
});
}.tabBar('场景二');
TabContent() {
Tabs() {
TabContent() {
ModificationV1();
}.tabBar('V1');
TabContent() {
ModificationV2();
}.tabBar('V2');
}
.height('100%')
.width('100%')
.scrollable(false)
.barMode(BarMode.Fixed)
.barBackgroundColor('#ffffff')
.barPosition(BarPosition.Start)
.onChange((index: number) => {
this.currentIndexIn3 = index;
});
}.tabBar('场景三');
TabContent() {
Tabs() {
TabContent() {
DecoratorV1();
}.tabBar('V1');
TabContent() {
DecoratorV2();
}.tabBar('V2');
}
.height('100%')
.width('100%')
.scrollable(false)
.barMode(BarMode.Fixed)
.barBackgroundColor('#ffffff')
.barPosition(BarPosition.Start)
.onChange((index: number) => {
this.currentIndexIn4 = index;
});
}.tabBar('场景四');
}
.height('100%')
.width('100%')
.scrollable(false)
.barMode(BarMode.Fixed)
.barBackgroundColor('#ffffff')
.barPosition(BarPosition.End)
.onChange((index: number) => {
this.currentIndexOut = index;
});
}
} 二、状态管理V2的局限性及注意事项。
三、状态管理混用与迁移场景介绍。
从实际角度出发,V1提出时间较早,有很大的基础,开发者无法一次性迁移成V2,因此在V1的代码中使用V2的部分能力是允许的,V2中也没有完全禁止V1。状态管理V1与V2的混用开发可以参考状态管理V1V2混用文档。API19新增的makeV1Observed以及enableV2Compatibility接口完善了V1与V2混用能力。
对希望将现有V1应用迁移到V2的开发者,可参考V1V2能力对比及迁移简表进行迁移,更详细的装饰器迁移示例可参考状态管理V1向V2迁移场景。
Q:@Event能实现的功能@Param都能实现,那为什么还要引入@Event?
A:一般来说,@Param主要用于变量传递,@Event用于函数传递,数据流向不一样,@Param是父传子,子组件不能更改父组件传递的数据,@Event是触发父组件更新数据。即使@Param传递的是函数,也必须赋初始值,而@Event不用初始化。
Q:AppStorage和AppStorageV2以及PersistentStorage和PersistentStorageV2是互通的吗?
A:AppStorage与AppStorageV2之间的数据互不共享,详情请参考AppStorageV2: 应用全局UI状态存储。
没有PersistentStorageV2,只有PersistenceV2,PersistentStorage和PersistenceV2存在差别,PersistentStorage功能上耦合了AppStorage,并且数据在不同module中使用也会有问题,因此推荐使用PersistenceV2的globalConnect接口替换掉PersistentStorage的persistProp接口。