鸿蒙应用开发实战【07】— ArkUI 状态管理全解析(@State/@Prop/@Link/@Watch)
·
鸿蒙应用开发实战【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
@Component
struct HomePage {
// @State 变量改变时,ArkUI 自动重新渲染关联的 UI 节点
@State loading: boolean = true
@State rows: AppRow[] = []
@State selectMode: boolean = false
@State selectedIds: number[] = []
build() {
Column() {
if (this.loading) {
LoadingProgress() // loading 为 true → 渲染这个
} else if (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: '已停用' } }
}
return r
})
ArkUI 响应式原理:
@State跟踪的是变量引用。基本类型赋新值、对象/数组赋新引用都会触发刷新;直接修改对象属性或 push 数组元素不会触发刷新。
三、@Prop — 父传子单向绑定
3.1 基本用法
// 父组件:AppListItem 的使用方
AppListItem({
appName: row.binding.app_name, // 传递属性
status: row.binding.status,
bindingId: row.binding.id ?? 0
})
// 子组件:AppListItem 的定义
@Component
export struct AppListItem {
@Prop appName: string = '' // 接收父组件属性,必须有默认值
@Prop status: string = '使用中'
@Prop bindingId: number = 0
build() {
Row() {
Text(this.appName) // 只读,不能修改父组件状态
StatusBadge({ status: this.status })
}
}
}
3.2 @Prop 的关键约束
// ❌ 不允许:在子组件中修改 @Prop 值来影响父组件
.onClick(() => {
this.appName = '新名字' // 子组件可以改自己的副本,但不同步到父
})
// ✅ 需要修改父数据时,使用回调函数模式
@Component
export struct AppListItem {
@Prop bindingId: number = 0
onDelete?: () => void // 回调函数,非 @Prop
build() {
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
@Component
struct AddCardPage {
@State selectedColorIndex: number = 0
build() {
Column() {
// 使用 $$ 传递 @Link 绑定
ColorPicker({ selectedIndex: $selectedColorIndex })
Text(`当前选中色块:${this.selectedColorIndex}`)
}
}
}
// 子组件:使用 @Link 接收双向绑定
@Component
struct ColorPicker {
@Link selectedIndex: 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
@Component
struct AddAppPage {
@State showCardSheet: 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
@Component
struct FilterDemo {
// @Watch 监听 filterCatIdx 变化,自动触发回调
@State @Watch('onFilterChanged') filterCatIdx: number = 0
@State @Watch('onFilterChanged') filterCardIdx: number = 0
@State @Watch('onFilterChanged') filterStatIdx: number = 0
@State filteredCount: number = 0
// 任何一个筛选条件变化时自动调用
onFilterChanged(propName: string): void {
console.info(`筛选条件变化:${propName}`)
// 重新计算过滤结果数量
this.filteredCount = this.getFilteredRows().length
}
}
5.2 @Watch 与直接计算的对比
// 方案A:@Watch 触发副作用(适合有异步操作的场景)
@State @Watch('saveFilter') filterCatIdx: number = 0
saveFilter(): void {
// 异步保存筛选状态到持久化
preferences.saveFilter(this.filterCatIdx)
}
// 方案B:直接计算属性(适合纯 UI 计算,更推荐)
private getFilteredRows(): AppRow[] {
return this.rows.filter((r: AppRow) => {
const catOk = this.filterCatIdx === 0 ||
r.binding.category === CATEGORIES[this.filterCatIdx]
// ...
return catOk
})
}
// 在 build() 中直接调用 this.getFilteredRows()
建议:纯 UI 相关的计算用
private方法直接计算(ArkUI 会在相关 @State 变化时自动重新调用);有副作用(网络、存储、日志)的操作才用@Watch。
六、号码助手中的状态管理实战
6.1 首页的完整状态设计
@Entry
@Component
struct HomePage {
// ── 数据状态 ──────────────────────────────────
@State loading: boolean = true // UI 加载态
@State rows: AppRow[] = [] // 绑定列表
@State cards: CardEntity[] = [] // 卡号列表(供筛选使用)
@State pendingSwitch: number = 0 // 待换绑数量
@State pendingCancel: number = 0 // 待注销数量
@State disabledCount: number = 0 // 已停用数量
// ── UI 交互状态 ──────────────────────────────
@State showFilter: boolean = false // 筛选弹窗是否显示
@State filterCatIdx: number = 0 // 分类筛选(0=全部)
@State filterCardIdx: number = 0 // 卡号筛选(0=全部)
@State filterStatIdx: number = 0 // 状态筛选(0=全部)
@State selectMode: boolean = false // 批量选择模式
@State selectedIds: number[] = [] // 已选 ID 列表
}
6.2 AddCardPage 的表单状态
@Entry
@Component
struct AddCardPage {
// 每个表单字段对应一个 @State
@State phone: string = ''
@State label: string = ''
@State selectedColorIndex: number = 0 // 0-4 对应 blue/purple/green/orange/pink
@State selectedCarrierIndex: number = 0 // 0-2 对应移动/联通/电信
@State saving: boolean = false // 防重复提交
// 表单校验:不需要 @State,直接计算
private canSave(): boolean {
return this.phone.trim().length === 11 &&
this.label.trim().length > 0 &&
!this.saving
}
}
6.3 AppDetailPage 的复合状态
@Entry
@Component
struct AppDetailPage {
// 页面数据状态(三态 UI)
@State loading: boolean = true
@State loadError: boolean = false
@State binding: 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 不检测深层属性变化
// ✅ 替换整个对象触发刷新
const updated: AppBindingEntity = { ...this.binding, status: '已停用' }
this.binding = updated
7.3 async 函数中的状态更新
// ❌ catch 中忘记更新 loading 状态,页面永久转圈
private async loadData(): Promise<void> {
this.loading = true
try {
this.rows = await AppBindingDao.listAll()
} catch (e) {
console.error(e)
// 忘记 this.loading = false!
}
this.loading = false // 如果 catch,这里不会执行
}
// ✅ 使用 finally 确保 loading 一定被关闭
private async loadData(): Promise<void> {
this.loading = true
try {
this.rows = await AppBindingDao.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 开发指南
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
更多推荐



所有评论(0)