HarmonyOS Next 状态管理:Computed 装饰器实践

目录

一. @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 的以下优势:

  1. 避免重复计算 :在 UI 中多次使用 totalPricehasDiscount 时,只会计算一次。
  2. 自动更新 :当依赖的状态变量(如 cart)发生变化时,@Computed 会自动重新计算。
  3. 代码简洁 :将复杂的计算逻辑封装在 @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 发生变化时,fahrenheitkelvin 会自动重新计算。
(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:学生姓名和成绩是可观察的状态变量。
相关推荐
全栈若城19 分钟前
87.HarmonyOS NEXT 单元测试与自动化测试指南:构建可靠的测试体系
华为·单元测试·harmonyos·harmonyos next
没有了遇见1 小时前
HarmonyOS学习ArkUI之线性布局 (Row/Column)
harmonyos·arkts
帅次2 小时前
Flutter FloatingActionButton 从核心用法到高级定制
android·flutter·macos·ios·kotlin·android-studio
别说我什么都不会2 小时前
OpenHarmony轻量系统服务管理|鸿蒙业务模型重要概念详解
物联网·嵌入式·harmonyos
枫叶丹42 小时前
【HarmonyOS Next之旅】DevEco Studio使用指南(三)
华为·harmonyos·harmonyos next
韩俊强3 小时前
Xcode16 Archive Error - Command SwiftCompile failed with a nonzero exit code
ios·xcode·swift·打包出错
个案命题4 小时前
打造流畅的下拉刷新与轮播交互:HarmonyOS手势识别与组件协同实战
华为·harmonyos·鸿蒙
我爱鸿蒙开发5 小时前
一文带你深入了解Stage模型
前端·架构·harmonyos
林钟雪5 小时前
HarmonyNext实战:基于ArkTS的跨平台文件管理系统开发
harmonyos