本文字数:约3500字 | 预计阅读时间:15分钟

前置知识:建议先学习本系列前三篇,特别是ArkTS语言基础和ArkUI组件布局

实战价值:掌握状态管理,才能构建出数据驱动、响应式的鸿蒙应用

系列导航:本文是《鸿蒙开发系列》第4篇,下篇将讲解网络请求与数据处理

一、状态管理概述:为什么需要状态管理?

在鸿蒙应用开发中,UI是随着数据的变化而变化的。当数据发生变化时,UI需要重新渲染以反映最新的数据。状态管理就是用来管理这些数据的变化,并确保UI能够及时更新。

ArkTS提供了多种状态管理装饰器,每种装饰器都有其特定的使用场景。理解这些装饰器的区别和用法,是构建复杂应用的基础。

二、@State:组件内部的状态

@State装饰的变量是组件内部的状态数据,当状态数据发生变化时,会触发UI重新渲染。

2.1 基本用法

typescript

@Entry
@Component
struct StateDemo {
  // 使用@State装饰器,表示count是组件的状态
  @State count: number = 0;

  build() {
    Column({ space: 20 }) {
      Text(`当前计数:${this.count}`)
        .fontSize(30)

      Button('增加')
        .onClick(() => {
          this.count++; // 修改状态,UI会自动更新
        })

      Button('减少')
        .onClick(() => {
          this.count--;
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

2.2 复杂对象的状态管理

typescript

// 定义对象类型
class User {
  name: string = '';
  age: number = 0;
}

@Entry
@Component
struct UserInfo {
  // 对象类型的状态
  @State user: User = new User();

  build() {
    Column({ space: 20 }) {
      Text(`姓名:${this.user.name}`)
      Text(`年龄:${this.user.age}`)

      Button('修改用户信息')
        .onClick(() => {
          // 直接修改对象的属性,UI不会更新
          // this.user.name = '张三';
          // this.user.age = 20;

          // 正确做法:创建一个新对象并整体赋值
          this.user = {
            name: '张三',
            age: 20
          } as User;
        })
    }
    .padding(20)
  }
}

注意:当@State装饰的对象属性发生变化时,需要整体赋值才能触发UI更新。如果只是修改对象的属性,UI不会更新。

三、@Prop:父子组件单向同步

@Prop装饰的变量是从父组件传递到子组件的,并且在子组件内部的变化不会同步回父组件,即单向同步。

3.1 基本用法

typescript

// 子组件
@Component
struct ChildComponent {
  // 使用@Prop装饰器,表示title是从父组件传递过来的
  @Prop title: string;

  build() {
    Column() {
      Text(this.title)
        .fontSize(20)

      Button('修改标题')
        .onClick(() => {
          this.title = '子组件修改后的标题'; // 仅子组件内变化,不会同步回父组件
        })
    }
    .padding(20)
    .border({ width: 1, color: Color.Gray })
  }
}

// 父组件
@Entry
@Component
struct ParentComponent {
  @State parentTitle: string = '父组件标题';

  build() {
    Column({ space: 20 }) {
      Text(`父组件标题:${this.parentTitle}`)
        .fontSize(20)

      // 将父组件的parentTitle传递给子组件的title
      ChildComponent({ title: this.parentTitle })

      Button('父组件修改标题')
        .onClick(() => {
          this.parentTitle = '父组件修改了标题'; // 父组件修改,会同步到子组件
        })
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

四、@Link:父子组件双向同步

@Link装饰的变量也是从父组件传递到子组件的,但是子组件内部的变化会同步回父组件,即双向同步。

4.1 基本用法

typescript

// 子组件
@Component
struct ChildComponent {
  // 使用@Link装饰器,表示title与父组件双向同步
  @Link title: string;

  build() {
    Column() {
      Text(this.title)
        .fontSize(20)

      Button('子组件修改标题')
        .onClick(() => {
          this.title = '子组件修改后的标题'; // 子组件修改,会同步回父组件
        })
    }
    .padding(20)
    .border({ width: 1, color: Color.Gray })
  }
}

// 父组件
@Entry
@Component
struct ParentComponent {
  @State parentTitle: string = '父组件标题';

  build() {
    Column({ space: 20 }) {
      Text(`父组件标题:${this.parentTitle}`)
        .fontSize(20)

      // 使用$符号创建引用,实现双向同步
      ChildComponent({ title: $parentTitle })

      Button('父组件修改标题')
        .onClick(() => {
          this.parentTitle = '父组件修改了标题'; // 父组件修改,会同步到子组件
        })
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

注意:在父组件中传递@Link变量时,需要使用$符号来创建引用。

五、@Watch:状态变化的监听器

@Watch装饰器用于监听状态变量的变化,当状态变量发生变化时,会触发指定的回调函数。

5.1 基本用法

typescript

@Entry
@Component
struct WatchDemo {
  @State count: number = 0;
  // 使用@Watch装饰器监听count的变化
  @Watch('onCountChange') @State watchedCount: number = 0;

  // 监听回调函数
  onCountChange(): void {
    console.log(`count发生变化,新值为:${this.count}`);
    // 可以在这里执行一些副作用,比如发送网络请求、保存数据等
  }

  build() {
    Column({ space: 20 }) {
      Text(`当前计数:${this.count}`)
        .fontSize(30)

      Button('增加')
        .onClick(() => {
          this.count++;
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

注意:@Watch装饰器需要放在状态装饰器(如@State)之前,并且监听的是同一个组件内的状态变量。

六、@Provide和@Consume:跨组件层级的数据同步

@Provide和@Consume装饰器用于跨组件层级的数据同步,不需要通过中间组件逐层传递。

6.1 基本用法

typescript

// 孙子组件
@Component
struct GrandChildComponent {
  // 使用@Consume装饰器,消费由祖先组件提供的值
  @Consume message: string;

  build() {
    Column() {
      Text(`孙子组件收到的消息:${this.message}`)
        .fontSize(20)

      Button('孙子组件修改消息')
        .onClick(() => {
          this.message = '孙子组件修改了消息'; // 修改会同步到所有消费该数据的组件
        })
    }
    .padding(20)
    .border({ width: 1, color: Color.Green })
  }
}

// 子组件
@Component
struct ChildComponent {
  build() {
    Column() {
      Text('这是子组件')
        .fontSize(20)

      GrandChildComponent()
    }
    .padding(20)
    .border({ width: 1, color: Color.Blue })
  }
}

// 父组件
@Entry
@Component
struct ParentComponent {
  // 使用@Provide装饰器,提供数据给后代组件
  @Provide message: string = '初始消息';

  build() {
    Column({ space: 20 }) {
      Text(`父组件消息:${this.message}`)
        .fontSize(20)

      ChildComponent()

      Button('父组件修改消息')
        .onClick(() => {
          this.message = '父组件修改了消息';
        })
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

使用@Provide和@Consume,可以在任意层级的组件之间进行数据同步,而不需要通过props逐层传递。

七、状态管理实战:购物车应用

下面我们通过一个购物车应用来综合运用上述状态管理装饰器。

7.1 定义数据模型

typescript

// 商品模型
class Product {
  id: number;
  name: string;
  price: number;
  image: string;

  constructor(id: number, name: string, price: number, image: string) {
    this.id = id;
    this.name = name;
    this.price = price;
    this.image = image;
  }
}

// 购物车项模型
class CartItem {
  product: Product;
  quantity: number;

  constructor(product: Product, quantity: number = 1) {
    this.product = product;
    this.quantity = quantity;
  }

  // 计算总价
  get totalPrice(): number {
    return this.product.price * this.quantity;
  }
}

7.2 商品列表组件

typescript

@Component
struct ProductItem {
  // 商品数据,从父组件传递,单向同步
  @Prop product: Product;
  // 添加购物车的回调函数
  onAddToCart: (product: Product) => void;

  build() {
    Row({ space: 12 }) {
      Image(this.product.image)
        .width(80)
        .height(80)
        .objectFit(ImageFit.Cover)

      Column({ space: 8 }) {
        Text(this.product.name)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)

        Text(`¥${this.product.price}`)
          .fontSize(18)
          .fontColor('#FF6B00')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      Button('加入购物车')
        .width(100)
        .height(36)
        .fontSize(14)
        .onClick(() => {
          this.onAddToCart(this.product);
        })
    }
    .width('100%')
    .padding(12)
    .backgroundColor(Color.White)
    .borderRadius(12)
    .shadow({ radius: 4, color: '#10000000' })
  }
}

7.3 购物车组件

typescript

@Component
struct CartItemComponent {
  // 购物车项数据,双向同步
  @Link cartItem: CartItem;
  // 删除购物车项的回调
  onDeleteItem: (item: CartItem) => void;

  build() {
    Row({ space: 12 }) {
      Image(this.cartItem.product.image)
        .width(60)
        .height(60)
        .objectFit(ImageFit.Cover)

      Column({ space: 4 }) {
        Text(this.cartItem.product.name)
          .fontSize(16)

        Text(`单价:¥${this.cartItem.product.price}`)
          .fontSize(14)
          .fontColor('#666666')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      Row({ space: 8 }) {
        Button('-')
          .width(30)
          .height(30)
          .fontSize(16)
          .onClick(() => {
            if (this.cartItem.quantity > 1) {
              this.cartItem.quantity--;
            }
          })

        Text(`${this.cartItem.quantity}`)
          .width(40)
          .textAlign(TextAlign.Center)

        Button('+')
          .width(30)
          .height(30)
          .fontSize(16)
          .onClick(() => {
            this.cartItem.quantity++;
          })
      }

      Text(`¥${this.cartItem.totalPrice}`)
        .width(80)
        .fontSize(16)
        .fontColor('#FF6B00')
        .textAlign(TextAlign.End)

      Button('删除')
        .width(60)
        .height(30)
        .fontSize(12)
        .backgroundColor('#FF3B30')
        .onClick(() => {
          this.onDeleteItem(this.cartItem);
        })
    }
    .width('100%')
    .padding(12)
    .backgroundColor(Color.White)
    .borderRadius(8)
  }
}

7.4 主页面

typescript

@Entry
@Component
struct ShoppingCartPage {
  // 商品列表状态
  @State products: Product[] = [
    new Product(1, '华为Mate 60', 6999, 'mate60.jpg'),
    new Product(2, '华为Watch 4', 2699, 'watch4.jpg'),
    new Product(3, '华为平板', 3299, 'tablet.jpg'),
  ];

  // 购物车状态
  @State cartItems: CartItem[] = [];

  // 计算总价
  get totalPrice(): number {
    return this.cartItems.reduce((sum, item) => sum + item.totalPrice, 0);
  }

  // 添加商品到购物车
  addToCart(product: Product) {
    const existingItem = this.cartItems.find(item => item.product.id === product.id);
    if (existingItem) {
      existingItem.quantity++;
    } else {
      this.cartItems.push(new CartItem(product, 1));
    }
  }

  // 从购物车删除商品
  deleteFromCart(itemToDelete: CartItem) {
    const index = this.cartItems.findIndex(item => item === itemToDelete);
    if (index !== -1) {
      this.cartItems.splice(index, 1);
    }
  }

  build() {
    Column({ space: 20 }) {
      // 商品列表
      Text('商品列表')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .textAlign(TextAlign.Start)

      Column({ space: 12 }) {
        ForEach(this.products, (product: Product) => {
          ProductItem({
            product: product,
            onAddToCart: (product: Product) => {
              this.addToCart(product);
            }
          })
        })
      }
      .width('100%')

      // 购物车
      Text(`购物车 (${this.cartItems.length})`)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .textAlign(TextAlign.Start)

      if (this.cartItems.length === 0) {
        Text('购物车空空如也')
          .fontSize(16)
          .fontColor('#999999')
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding(40)
      } else {
        Column({ space: 12 }) {
          ForEach(this.cartItems, (item: CartItem) => {
            CartItemComponent({
              cartItem: $item, // 使用$创建引用,实现双向同步
              onDeleteItem: (item: CartItem) => {
                this.deleteFromCart(item);
              }
            })
          })

          // 总计
          Row() {
            Text('总计:')
              .fontSize(18)

            Blank()

            Text(`¥${this.totalPrice}`)
              .fontSize(24)
              .fontColor('#FF6B00')
              .fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .padding({ top: 20, bottom: 20 })

          Button('去结算')
            .width('100%')
            .height(50)
            .fontSize(18)
            .backgroundColor('#007DFF')
            .fontColor(Color.White)
        }
        .width('100%')
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F5F5F5')
  }
}

八、状态管理最佳实践

8.1 状态提升

将多个组件需要共享的状态提升到最近的共同父组件中管理。

8.2 合理选择装饰器

  • 组件内部状态:@State

  • 父子组件单向同步:@Prop

  • 父子组件双向同步:@Link

  • 跨组件层级同步:@Provide和@Consume

  • 监听状态变化:@Watch

8.3 避免不必要的渲染

  • 使用@State时,对于对象类型,避免直接修改属性,应该整体赋值

  • 使用@Prop时,如果父组件频繁更新但子组件不需要重新渲染,可以考虑使用@Link或@Consume

8.4 状态分离

将状态逻辑与UI分离,可以使用自定义类或函数来管理复杂的状态逻辑。

九、常见问题与解决方案

问题1:@State装饰的对象属性变化,UI不更新

原因:直接修改了对象的属性,而不是整体赋值。
解决:创建一个新对象并整体赋值。

问题2:@Prop和@Link的区别

  • @Prop是单向同步:父组件到子组件

  • @Link是双向同步:父组件和子组件相互同步

问题3:@Watch回调函数中修改状态导致无限循环

解决:确保@Watch回调函数中修改的状态不会再次触发同一个@Watch回调。

十、总结与下期预告

10.1 本文要点回顾

  1. @State:组件内部状态,变化触发UI更新

  2. @Prop:父子组件单向同步

  3. @Link:父子组件双向同步

  4. @Watch:监听状态变化

  5. @Provide和@Consume:跨组件层级同步

  6. 实战:购物车应用的综合运用

10.2 下期预告:《鸿蒙开发之:网络请求与数据处理》

下篇文章将深入讲解:

  • 使用HTTP模块进行网络请求

  • 处理JSON数据

  • 异步编程:Promise和async/await

  • 数据缓存策略

  • 实战:构建一个新闻客户端


动手挑战

任务1:实现一个计数器应用
要求:

  • 包含两个计数器A和B

  • 计数器A每增加10,计数器B自动增加1(使用@Watch)

  • 提供重置按钮,重置两个计数器

任务2:实现一个任务管理应用
要求:

  • 可以添加、删除、标记任务完成

  • 显示未完成任务数量和总任务数量

  • 使用@Provide和@Consume实现状态共享

任务3:优化购物车应用
要求:

  • 增加商品库存概念,购买数量不能超过库存

  • 实现购物车本地持久化(可以使用LocalStorage)

  • 增加优惠券功能,结算时自动扣减

将你的代码分享到评论区,我会挑选优秀实现进行详细点评!


常见问题解答

Q:@State和@Link可以一起使用吗?
A:可以。@State用于管理组件内部状态,@Link用于与子组件双向同步。在父组件中使用@State,然后通过$符号传递给子组件的@Link。

Q:@Watch可以监听多个状态变量吗?
A:可以。每个状态变量都可以有自己的@Watch装饰器,分别指定不同的回调函数。

Q:@Provide和@Consume与@Link有什么区别?
A:@Provide和@Consume用于跨任意组件层级的数据同步,而@Link只能在父子组件之间使用。@Provide和@Consume更适合全局状态管理。

Q:状态管理会导致性能问题吗?
A:不合理的使用状态管理可能会导致不必要的UI渲染,从而影响性能。建议遵循最佳实践,如状态提升、合理选择装饰器等。


PS:现在HarmonyOS应用开发者认证正在做活动,初级和高级都可以免费学习及考试,赶快加入班级学习啦:【注意,考试只能从此唯一链接进入】
https://developer.huawei.com/consumer/cn/training/classDetail/33f85412dc974764831435dc1c03427c?type=1?ha_source=hmosclass&ha_sourceld=89000248

版权声明:本文为《鸿蒙开发系列》第4篇,原创文章,转载请注明出处。

标签:#HarmonyOS #鸿蒙开发 #状态管理 #数据绑定 #ArkUI #华为开发者

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐