本文字数:约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 #华为开发者