鸿蒙开发之:状态管理与数据绑定

60 阅读8分钟

本文字数:约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 本文要点回顾 @State:组件内部状态,变化触发UI更新

@Prop:父子组件单向同步

@Link:父子组件双向同步

@Watch:监听状态变化

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

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

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应用开发者认证正在做活动,初级和高级都可以免费学习及考试,赶快加入班级学习啦:【注意,考试只能从此唯一链接进入】 developer.huawei.com/consumer/cn…

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

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