鸿蒙应用开发实战【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 | 附加在其他装饰器上 | 监听变化触发回调 | 状态变化时执行副作用 |
@StorageProp | AppStorage→组件 | 单向(全局→组件) | 读取全局状态 |
@StorageLink | AppStorage↔组件 | 双向(全局↔组件) | 读写全局状态 |
@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[] | ✅ |
null | string | 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=updated7.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