鸿蒙应用开发实战【07】— ArkUI 状态管理全解析(@State/@Prop/@Link/@Watch)
2026/7/15 23:16:46 网站建设 项目流程

鸿蒙应用开发实战【07】— ArkUI 状态管理全解析(@State/@Prop/@Link/@Watch)

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

ArkUI 的响应式系统是其最核心的特性之一——当被装饰器标记的变量发生变化时,ArkUI 框架会自动、精确地重新渲染受影响的 UI 节点,无需手动调用setState或触发更新。本篇全面讲解 ArkUI 的状态管理体系,结合号码助手项目中的真实代码,深入理解每个装饰器的使用场景和注意事项。

本篇涵盖:@State(组件内状态)、@Prop(父传子只读)、@Link(双向绑定)、@Watch(变量监听)、@Observed + @ObjectLink(嵌套对象)、以及常见的状态更新陷阱。


一、ArkUI 状态管理体系全览

1.1 装饰器一览表

装饰器作用域数据流向适用场景
@State组件内单向(内部驱动 UI)组件自己的状态
@Prop父传子单向(父→子,子只读)父组件传递配置
@Link父传子双向(父↔子同步)父子共享可变状态
@Watch附加在其他装饰器上监听变化触发回调状态变化时执行副作用
@StoragePropAppStorage→组件单向(全局→组件)读取全局状态
@StorageLinkAppStorage↔组件双向(全局↔组件)读写全局状态
@Provide祖先→后代单向(跨层传递)跨多层组件传数据
@Consume接收 @Provide双向(自动关联)消费祖先注入的数据
@Observed类声明使对象属性可被监听
@ObjectLink子组件双向(监听对象属性)列表项组件编辑

1.2 号码助手中使用的装饰器统计

@State: HomePage(10个)、AddCardPage(6个)、AppDetailPage(3个)、... @Prop: AppListItem(6个)、AvatarBadge(5个)、StatusBadge(1个) @StorageProp: AppDetailPage(1个) @Watch: (当前未使用,可用于监听筛选条件变化)

二、@State — 组件内响应式状态

2.1 基本用法

@Entry@Componentstruct HomePage{// @State 变量改变时,ArkUI 自动重新渲染关联的 UI 节点@Stateloading:boolean=true@Staterows:AppRow[]=[]@StateselectMode:boolean=false@StateselectedIds:number[]=[]build(){Column(){if(this.loading){LoadingProgress()// loading 为 true → 渲染这个}elseif(this.rows.length===0){Text('暂无数据')// rows 空 → 渲染这个}else{// rows 有数据 → 渲染列表ForEach(this.rows,(row:AppRow)=>{Text(row.binding.app_name)},(row:AppRow)=>`${row.binding.id??0}`)}}}}

2.2 @State 支持的类型

类型示例是否支持
基本类型boolean / number / string
对象类型CardEntity / AppRow
数组类型number[] / AppRow[]
nullstring | null = null
any❌(ArkTS 禁止 any)
undefined⚠️(需谨慎,建议用 null)

2.3 @State 数组更新的正确姿势

// ❌ 直接 push 不触发 UI 刷新(引用未变)this.selectedIds.push(newId)// ✅ 使用 concat 创建新数组(引用变化触发刷新)this.selectedIds=this.selectedIds.concat([newId])// ❌ 直接修改数组元素this.rows[0].binding.status='已停用'// ✅ 替换整个数组(或使用 @Observed + @ObjectLink)this.rows=this.rows.map((r:AppRow)=>{if(r.binding.id===targetId){return{...r,binding:{...r.binding,status:'已停用'}}}returnr})

ArkUI 响应式原理@State跟踪的是变量引用。基本类型赋新值、对象/数组赋新引用都会触发刷新;直接修改对象属性或 push 数组元素不会触发刷新。


三、@Prop — 父传子单向绑定

3.1 基本用法

// 父组件:AppListItem 的使用方AppListItem({appName:row.binding.app_name,// 传递属性status:row.binding.status,bindingId:row.binding.id??0})// 子组件:AppListItem 的定义@Componentexportstruct AppListItem{@PropappName:string=''// 接收父组件属性,必须有默认值@Propstatus:string='使用中'@PropbindingId:number=0build(){Row(){Text(this.appName)// 只读,不能修改父组件状态StatusBadge({status:this.status})}}}

3.2 @Prop 的关键约束

// ❌ 不允许:在子组件中修改 @Prop 值来影响父组件.onClick(()=>{this.appName='新名字'// 子组件可以改自己的副本,但不同步到父})// ✅ 需要修改父数据时,使用回调函数模式@Componentexportstruct AppListItem{@PropbindingId:number=0onDelete?:()=>void// 回调函数,非 @Propbuild(){Row(){Button('删除').onClick(()=>{if(this.onDelete){this.onDelete()// 通知父组件}})}}}// 父组件使用回调AppListItem({bindingId:row.binding.id??0,onDelete:()=>{this.deleteBinding(row.binding.id??0)}})

四、@Link — 父子双向绑定

4.1 基本用法

// 父组件:将 @State 变量通过 $$ 符号传递给子组件@Entry@Componentstruct AddCardPage{@StateselectedColorIndex:number=0build(){Column(){// 使用 $$ 传递 @Link 绑定ColorPicker({selectedIndex:$selectedColorIndex})Text(`当前选中色块:${this.selectedColorIndex}`)}}}// 子组件:使用 @Link 接收双向绑定@Componentstruct ColorPicker{@LinkselectedIndex:number// 双向绑定,修改会同步到父build(){Row(){ForEach([0,1,2,3,4],(i:number)=>{Circle().onClick(()=>{this.selectedIndex=i})// 修改会同步到父组件},(i:number)=>`${i}`)}}}

4.2 @Prop vs @Link 对比

特性@Prop@Link
数据流向父→子(单向)父↔子(双向)
子修改后父同步❌ 不同步✅ 同步
父修改后子刷新✅ 刷新✅ 刷新
传递方式直接传值$$传引用
适用场景展示型子组件交互型子组件

4.3 bindSheet 使用 @Link 的典型场景

// AddAppPage.ets — bindSheet 使用 @Link 控制显示状态@Entry@Componentstruct AddAppPage{@StateshowCardSheet:boolean=false// @State 控制底部抽屉build(){Column(){// 点击按钮打开底部抽屉Button('选择卡号').onClick(()=>{this.showCardSheet=true})}// bindSheet 使用 $$ 绑定(内部使用 @Link 语义).bindSheet($$this.showCardSheet,this.CardPickerSheet,{height:SheetSize.FIT_CONTENT,preferType:SheetType.BOTTOM})}}

五、@Watch — 状态变化监听

5.1 基本用法

@Entry@Componentstruct FilterDemo{// @Watch 监听 filterCatIdx 变化,自动触发回调@State@Watch('onFilterChanged')filterCatIdx:number=0@State@Watch('onFilterChanged')filterCardIdx:number=0@State@Watch('onFilterChanged')filterStatIdx:number=0@StatefilteredCount:number=0// 任何一个筛选条件变化时自动调用onFilterChanged(propName:string):void{console.info(`筛选条件变化:${propName}`)// 重新计算过滤结果数量this.filteredCount=this.getFilteredRows().length}}

5.2 @Watch 与直接计算的对比

// 方案A:@Watch 触发副作用(适合有异步操作的场景)@State@Watch('saveFilter')filterCatIdx:number=0saveFilter():void{// 异步保存筛选状态到持久化preferences.saveFilter(this.filterCatIdx)}// 方案B:直接计算属性(适合纯 UI 计算,更推荐)privategetFilteredRows():AppRow[]{returnthis.rows.filter((r:AppRow)=>{constcatOk=this.filterCatIdx===0||r.binding.category===CATEGORIES[this.filterCatIdx]// ...returncatOk})}// 在 build() 中直接调用 this.getFilteredRows()

建议:纯 UI 相关的计算用private方法直接计算(ArkUI 会在相关 @State 变化时自动重新调用);有副作用(网络、存储、日志)的操作才用@Watch


六、号码助手中的状态管理实战

6.1 首页的完整状态设计

@Entry@Componentstruct HomePage{// ── 数据状态 ──────────────────────────────────@Stateloading:boolean=true// UI 加载态@Staterows:AppRow[]=[]// 绑定列表@Statecards:CardEntity[]=[]// 卡号列表(供筛选使用)@StatependingSwitch:number=0// 待换绑数量@StatependingCancel:number=0// 待注销数量@StatedisabledCount:number=0// 已停用数量// ── UI 交互状态 ──────────────────────────────@StateshowFilter:boolean=false// 筛选弹窗是否显示@StatefilterCatIdx:number=0// 分类筛选(0=全部)@StatefilterCardIdx:number=0// 卡号筛选(0=全部)@StatefilterStatIdx:number=0// 状态筛选(0=全部)@StateselectMode:boolean=false// 批量选择模式@StateselectedIds:number[]=[]// 已选 ID 列表}

6.2 AddCardPage 的表单状态

@Entry@Componentstruct AddCardPage{// 每个表单字段对应一个 @State@Statephone:string=''@Statelabel:string=''@StateselectedColorIndex:number=0// 0-4 对应 blue/purple/green/orange/pink@StateselectedCarrierIndex:number=0// 0-2 对应移动/联通/电信@Statesaving:boolean=false// 防重复提交// 表单校验:不需要 @State,直接计算privatecanSave():boolean{returnthis.phone.trim().length===11&&this.label.trim().length>0&&!this.saving}}

6.3 AppDetailPage 的复合状态

@Entry@Componentstruct AppDetailPage{// 页面数据状态(三态 UI)@Stateloading:boolean=true@StateloadError:boolean=false@Statebinding:AppBindingEntity|null=null// 从全局 AppStorage 读取传递的 ID@StorageProp('current_binding_id')bindingId:number=0}

七、状态更新常见陷阱

7.1 ForEach key 函数影响更新效率

// ❌ key 函数只用 index:导致数组顺序变化时全量重渲染ForEach(this.rows,(row:AppRow)=>{AppListItem({...})},(row:AppRow,index:number)=>`${index}`)// 仅用 index 作为 key// ✅ key 函数包含数据特征:精确定位需要更新的列表项ForEach(this.rows,(row:AppRow)=>{AppListItem({...})},(row:AppRow)=>`binding_${row.binding.id??0}_${row.binding.status}`)// ↑ 包含 id(唯一标识)和 status(影响显示的状态)

7.2 @State 对象深层属性不触发更新

// ❌ 修改对象深层属性不触发刷新this.binding.status='已停用'// ArkUI 不检测深层属性变化// ✅ 替换整个对象触发刷新constupdated:AppBindingEntity={...this.binding,status:'已停用'}this.binding=updated

7.3 async 函数中的状态更新

// ❌ catch 中忘记更新 loading 状态,页面永久转圈privateasyncloadData():Promise<void>{this.loading=truetry{this.rows=awaitAppBindingDao.listAll()}catch(e){console.error(e)// 忘记 this.loading = false!}this.loading=false// 如果 catch,这里不会执行}// ✅ 使用 finally 确保 loading 一定被关闭privateasyncloadData():Promise<void>{this.loading=truetry{this.rows=awaitAppBindingDao.listAll()}catch(e){console.error(e)}finally{this.loading=false// ✅ 无论成功失败都执行}}

八、本篇小结

装饰器核心用途号码助手中的使用
@State组件内响应式状态loading、rows、selectMode 等
@Prop父传子只读属性AppListItem 的 appName、status 等
@Link父子双向绑定bindSheet 的 showCardSheet
@Watch状态变化副作用可用于监听筛选条件保存
@StorageProp全局状态只读AppDetailPage 的 bindingId

最重要的记忆点:数组/对象的深层修改不触发 @State 刷新,必须替换整个引用(this.arr = [...newArr])。


参考资料

  • 鸿蒙应用开发实战【05】— 首页开发与状态管理
  • 鸿蒙应用开发实战【08】— AppStorage 全局状态
  • @State 装饰器
  • @Prop 装饰器
  • @Link 装饰器
  • @Watch 装饰器
  • 状态管理概述
  • ForEach 开发指南

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

  • 开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
  • ArkUI 状态管理文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-state-management-overview
  • @State 装饰器:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-state

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询