目录
- HarmonyOS Next 状态管理:Local 装饰器实践
- HarmonyOS Next 状态管理:Param 装饰器实践
- HarmonyOS Next 状态管理:Once 装饰器实践
- HarmonyOS Next 状态管理:Event 装饰器实践
- HarmonyOS Next 状态管理:!! 状态装饰器实践
- HarmonyOS Next 状态管理:@ObserverV2和@Trace 装饰器实践
- HarmonyOS Next 状态管理:Provider和Consumer 装饰器实践
- HarmonyOS Next 状态管理:Monitor 装饰器实践
- HarmonyOS Next 状态管理:Computed 装饰器实践
- # HarmonyOS Next 状态管理:Type 装饰器实践
一. @Computed 修饰器概述
@Computed
是 ArkTS 中的一个装饰器,用于定义计算属性 。它的主要作用是优化性能,避免在 UI 中多次重复计算相同的值。当依赖的状态变量发生变化时,@Computed
会自动重新计算,但只会计算一次,从而减少不必要的性能开销。
二. @Computed 修饰器的限制
@Computed
只能在ComponentV2
中使用。@Computed
只能用于装饰getter
方法,不能用于普通方法或属性。- 在
@Computed
装饰的getter
方法中,不能修改参与计算的状态变量。 @Computed
装饰的属性是只读的,不能用于双向绑定。
三、实践探索
3.1 简单使用
less
@Entry
@ComponentV2
struct Index {
@Local firstName: string = "李"
@Local lastName: string = "雷"
@Computed
get fullName() {
console.info("---------Computed----------");
console.info("first:", this.firstName);
console.info("last:", this.lastName);
return this.firstName + this.lastName
}
build() {
Column({space: 20}) {
Row() {
Text(this.fullName)
Text(this.fullName)
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
Button('changed lastName').onClick(() => {
this.lastName += `${Math.round(Math.random() * 1000)}`;
})
}
.width('100%')
.height('100%')
}
}
less
03-16 21:35:17.342 23056-23056 A00000/testTag pid-23056 I Succeeded in loading the content.
03-16 21:35:17.344 23056-23056 A03d00/JSAPP pid-23056 I ---------Computed----------
03-16 21:35:17.344 23056-23056 A03d00/JSAPP pid-23056 I first: 李
03-16 21:35:17.344 23056-23056 A03d00/JSAPP pid-23056 I last: 雷
03-16 21:35:28.956 23056-23056 A03d00/JSAPP com.examp...lication I ---------Computed----------
03-16 21:35:28.956 23056-23056 A03d00/JSAPP com.examp...lication I first: 李
03-16 21:35:28.956 23056-23056 A03d00/JSAPP com.examp...lication I last: 雷267
关键点 : 有两个Text(this.fullName)关联了this.fullName,在冷启的时候,有2次首次渲染,应该有2日志的输出;再加上点击按钮事件,改变了@Local的值会触发UI刷新,理论上会再有2次;总共应该有4次,但实际上只有2次,一次是初始化触发的,一次是更新lastName触发。这里 @Computed
的作用就是避免重复计算,对于相同的计算结果,只会触发一次。
- 初始化首次渲染
less
03-16 21:35:17.344 23056-23056 A03d00/JSAPP pid-23056 I ---------Computed----------
03-16 21:35:17.344 23056-23056 A03d00/JSAPP pid-23056 I first: 李
03-16 21:35:17.344 23056-23056 A03d00/JSAPP pid-23056 I last: 雷
- 点击按钮刷新
less
03-16 21:35:28.956 23056-23056 A03d00/JSAPP com.examp...lication I ---------Computed----------
03-16 21:35:28.956 23056-23056 A03d00/JSAPP com.examp...lication I first: 李
03-16 21:35:28.956 23056-23056 A03d00/JSAPP com.examp...lication I last: 雷267
3.2 实战场景: 购物车计算总价和折扣
场景描述:
- 用户可以在购物车中添加商品,每个商品有单价和数量。
- 计算购物车的总价。
- 如果总价超过一定金额(如 100 元),用户可以享受折扣。
- 使用
@Computed
来计算总价和折扣状态,避免重复计算。
typescript
// 定义商品类
@ObservedV2
class Product {
@Trace quantity: number = 0; // 商品数量
unitPrice: number = 0; // 商品单价
constructor(quantity: number, unitPrice: number) {
this.quantity = quantity;
this.unitPrice = unitPrice;
}
}
// 主组件
@Entry
@ComponentV2
struct ShoppingCart {
@Local cart: Product[] = [
new Product(2, 30), // 商品1:单价30元,数量2
new Product(3, 20), // 商品2:单价20元,数量3
new Product(1, 50), // 商品3:单价50元,数量1
];
// 计算总价
@Computed
get totalPrice(): number {
console.info("Calculating total price...");
return this.cart.reduce((sum, product) => sum + product.quantity * product.unitPrice, 0);
}
// 判断是否享受折扣
@Computed
get hasDiscount(): boolean {
console.info("Checking discount eligibility...");
return this.totalPrice >= 100; // 总价超过100元享受折扣
}
build() {
Column({ space: 10 }) {
// 显示购物车中的商品
ForEach(this.cart, (product: Product, index: number) => {
Row({ space: 10 }) {
Text(`商品 ${index + 1}: 单价 ${product.unitPrice} 元`)
Button('-')
.onClick(() => {
if (product.quantity > 0) {
product.quantity--; // 减少商品数量
}
})
Text(`数量: ${product.quantity}`)
Button('+')
.onClick(() => {
product.quantity++; // 增加商品数量
})
}
.margin({ bottom: 10 })
})
Divider()
// 显示总价和折扣信息
Text(`总价: ${this.totalPrice.toFixed(2)} 元`)
.fontSize(20)
.fontColor(this.hasDiscount ? Color.Red : Color.Black)
Text(this.hasDiscount ? "享受折扣!" : "未达到折扣条件")
.fontSize(16)
.fontColor(Color.Gray)
}
.padding(20)
.width('100%')
}
}
Computed
的以下优势:
- 避免重复计算 :在 UI 中多次使用
totalPrice
和hasDiscount
时,只会计算一次。 - 自动更新 :当依赖的状态变量(如
cart
)发生变化时,@Computed
会自动重新计算。 - 代码简洁 :将复杂的计算逻辑封装在
@Computed
中,使 UI 代码更加清晰。
3.2 @Computed
和 @Monitor
结合使用
typescript
@Entry
@ComponentV2
struct TemperatureConverter {
@Local celsius: number = 25
// 计算华氏温度
@Computed
get fahrenheit(): number {
console.info("Calculating Fahrenheit...");
return this.celsius * 9 / 5 + 32
}
// 计算开尔文温度
@Computed
get kelvin(): number {
console.info("Calculating Kelvin...");
return this.celsius + 273.15
}
// 监听开尔文温度的变化
@Monitor('kelvin')
onKelvinChange(monitor: IMonitor) {
monitor.dirty.forEach((path) => {
console.log(`${path}, before:${monitor.value(path)?.before}, now:${monitor.value(path)?.now}`)
})
}
build() {
Column({ space: 20 }) {
TextInput({text: this.celsius.toString()})
.onChange((value: string) => {
this.celsius = parseFloat(value) || 0
})
.placeholderColor('请输入摄氏温度')
.margin({ bottom: 20 })
Text(`华氏温度: ${this.fahrenheit.toFixed(2)}°F`)
.fontSize(20)
.margin({ bottom: 10})
Text(`开尔文温度: ${this.kelvin.toFixed(2)}K`)
.fontSize(20)
.margin({ bottom: 10 })
Button('增加温度')
.onClick(() => {
this.celsius += 1
})
.margin({ bottom: 10 })
Button('减少温度')
.onClick(() => {
this.celsius -= 1
})
}
.padding(20)
.width('100%')
}
}
代码拆解
(1) @Computed
计算属性
fahrenheit
:根据摄氏温度计算华氏温度。kelvin
:根据摄氏温度计算开尔文温度。- 当
celsius
发生变化时,fahrenheit
和kelvin
会自动重新计算。
(2) @Monitor
监听状态变化
onKelvinChange
:监听kelvin
的变化。- 当
kelvin
的值发生变化时,会触发onKelvinChange
方法,并打印变化前后的值。
(3)UI 交互
- 通过
TextInput
输入摄氏温度。 - 点击"增加温度"或"减少温度"按钮,可以调整摄氏温度。
- 华氏温度和开尔文温度会实时更新。
3.2 @Computed
和 @Param
结合使用
@Computed
和 @Param
结合使用的场景通常出现在父子组件之间,父组件通过 @Param
向子组件传递数据,而子组件可以使用 @Computed
对这些数据进行计算或处理。
typescript
@ObservedV2
class Student {
@Trace name: string; // 学生姓名
@Trace score: number; // 学生成绩
constructor(name: string, score: number) {
this.name = name;
this.score = score;
}
}
@Entry
@ComponentV2
struct ParentComponent {
@Local students: Student[] = [
new Student("Alice", 85),
new Student("Bob", 50),
new Student("Cathy", 58),
];
// 计算平均成绩
@Computed
get averageScore(): number {
console.info("Calculating average score...");
const total = this.students.reduce((sum, student) => sum + student.score, 0);
return total / this.students.length;
}
build() {
Column({ space: 10 }) {
// 显示学生成绩列表
ForEach(this.students, (student: Student, index: number) => {
Row({ space: 10 }) {
Text(`学生 ${index + 1}: ${student.name}`)
Text(`成绩: ${student.score}`)
Button('+5')
.onClick(() => {
student.score += 5; // 增加成绩
})
Button('-5')
.onClick(() => {
student.score -= 5; // 减少成绩
})
}
.margin({ bottom: 10 })
})
Divider()
// 将平均成绩传递给子组件
ChildComponent({ averageScore: this.averageScore })
}
.padding(20)
.width('100%')
}
}
@ComponentV2
struct ChildComponent {
@Param averageScore: number = 0;
// 判断是否及格
@Computed
get isPass(): boolean {
console.info("Checking pass status...");
return this.averageScore >= 60;
}
build() {
Column({ space: 10 }) {
Text(`平均成绩: ${this.averageScore.toFixed(2)}`)
.fontSize(20)
Text(this.isPass ? "及格!" : "不及格!")
.fontSize(16)
.fontColor(this.isPass ? Color.Green : Color.Red)
}
}
}
代码解析
(1)父组件
- 维护一个学生成绩列表
students
,每个学生有姓名和成绩。 - 通过
ForEach
渲染学生成绩列表,并提供按钮调整成绩。 - 使用
@Computed
计算平均成绩averageScore
。 - 将平均成绩通过
@Param
传递给子组件ChildComponent
。
(2)子组件
- 通过
@Param
接收父组件传递的平均成绩averageScore
。 - 使用
@Computed
判断是否及格isPass
。 - 在 UI 中显示平均成绩和是否及格。
(3)学生类
- 使用
@ObservedV2
装饰,表示这是一个可观察的类。 @Trace name
和@Trace score
:学生姓名和成绩是可观察的状态变量。