ArkTS状态管理:@State、@Link、@Prop

知识点讲解
一、为什么需要这些装饰器?
在 ArkTS 里,UI 是用代码"画"出来的。如果数据变了,我们不可能手动去刷新界面 —— 太麻烦。
状态装饰器就是自动刷新机制的开关:只要给变量加上 @State 之类的前缀,数据一变,界面自动重绘。
但组件之间会嵌套,父组件的数据要传给子组件,这时就出现了三种流向:
| 装饰器 | 角色 | 数据流向 |
|---|---|---|
@State |
父(数据源) | 自己拥有数据 |
@Link |
子(双向) | 父 ⇄ 子 双向同步 |
@Prop |
子(单向) | 父 → 子 单向传递 |
二、一个生活化的比喻
把状态想象成一份文件:
- 📄 @State = 文件的原件(只一份,放在父组件抽屉里)
- 🔗 @Link = 子组件拿了钥匙,直接打开父的抽屉读写 → 父改子看到,子改父也看到
- 📋 @Prop = 子组件拿了复印件回去 → 父改原件,子下次拿到新复印件;但子在复印件上乱写,原件不受影响
记住这个比喻,后面代码就很好懂。
三、@State:数据源头
@State 是状态的"主人"。它声明在哪个组件,数据就归谁所有。
最小例子
@Entry
@Component
struct Index {
@State count: number = 0; // 声明状态
build() {
Column() {
Text('count = ' + this.count) // 自动随 count 变化
Button('+1')
.onClick(() => { this.count++; }) // 改了 count,Text 自动刷新
}
}
}
要点:
- 只能在当前组件修改
@State变量。 - 一旦它的值变化,所有引用它的 UI 都会重新渲染。
- 它是后续
@Link和@Prop的数据源 —— 没有它,后两者无从谈起。
四、@Link:双向同步
@Link 让子组件直接共享父的 @State,任一方修改,另一方立即同步。
代码
@Entry
@Component
struct Parent {
@State count: number = 0;
build() {
Column() {
Text('父: ' + this.count)
Child({ count: $count }) // 注意 $ 前缀:传引用
}
}
}
@Component
struct Child {
@Link count: number; // 接收引用,不能给默认值
build() {
Column() {
Text('子: ' + this.count)
Button('子 +1')
.onClick(() => { this.count++; }) // 子改了,父也变
}
}
}
关键细节
| 项 | 说明 |
|---|---|
| 父传参写法 | Child({ count: $count }) —— 必须用 $ 取引用 |
| 子声明 | @Link count: number —— 不能赋初值 |
| 类型要求 | 必须与父 @State 类型完全一致 |
| 支持类型 | number / string / boolean / object / array 都行 |
数据流向图
父 @State count ─────$引用─────► 子 @Link count
▲ │
└──────── 同步改动 ◄───────────┘
(子改 → 父也变)
五、@Prop:单向传递
@Prop 让子组件拿到父数据的副本。父改 → 子会刷新;但子改 → 父不知道。
代码
@Entry
@Component
struct Parent {
@State count: number = 0;
build() {
Column() {
Text('父: ' + this.count)
Child({ count: this.count }) // 直接传值(注意没有 $)
}
}
}
@Component
struct Child {
@Prop count: number; // 接收副本
build() {
Column() {
Text('子: ' + this.count)
Button('子 +1')
.onClick(() => { this.count++; }) // 只改自己的副本,父不变
}
}
}
关键细节
| 项 | 说明 |
|---|---|
| 父传参写法 | Child({ count: this.count }) —— 传值,无 $ |
| 子声明 | @Prop count: number —— 可有默认值 |
| 类型要求 | 只支持简单类型(number / string / boolean) |
| 不支持 | 对象、数组(这些必须用 @Link) |
数据流向图
父 @State count ──────值拷贝──────► 子 @Prop count
│
▼
子改只影响自己,父不变 ✗
六、三者对比总表
| 维度 | @State | @Link | @Prop |
|---|---|---|---|
| 谁拥有数据 | 当前组件 | 父组件 | 父组件 |
| 数据传递 | — | 引用($count) |
值(this.count) |
| 父→子 | — | ✅ 同步 | ✅ 同步 |
| 子→父 | — | ✅ 同步 | ❌ 不同步 |
| 子能否给初值 | ✅ | ❌ | ✅ |
| 支持对象/数组 | ✅ | ✅ | ❌ |
| 典型场景 | 数据源头 | 父子共同操作同一数据 | 只读展示 |
七、什么时候用哪个?
三个判断问题:
- 数据归谁所有? → 归谁就用
@State声明。 - 子组件需不需要改父的数据?
- 需要 → 用
@Link(双向) - 不需要 → 用
@Prop(单向)
- 需要 → 用
- 数据是对象/数组吗?
- 是 → 必须
@Link(@Prop不支持) - 不是 →
@Link或@Prop都行
- 是 → 必须
常见搭配
✅ 父组件计数器 → 子组件也能加 → @State + @Link
✅ 父组件主题色 → 子组件只显示 → @State + @Prop
✅ 父组件用户对象 → 子组件可编辑 → @State + @Link(@Prop 不支持对象)
八、新手最容易踩的坑
❌ 坑 1:用 @Prop 传对象
// 父
@State user: User = { name: 'Tom', age: 18 };
Child({ user: this.user }) // ❌ 编译报错,@Prop 不支持对象
// 改用 @Link
Child({ user: $user }) // ✅
❌ 坑 2:@Link 给了默认值
@Link count: number = 0; // ❌ 不允许
@Link count: number; // ✅ 必须由父初始化
❌ 坑 3:忘记 $
Child({ count: this.count }) // ❌ 这是传值(@Prop 用法)
Child({ count: $count }) // ✅ @Link 必须用 $
❌ 坑 4:类型不一致
@State count: number = 0;
@Link count: string; // ❌ 父 number 子 string,编译报错
九、一句话记忆
@State 是原件, @Link 是钥匙(共享双向), @Prop 是复印件(只读单向)。
十、动手试一试
已实现了一个并排对比的演示页面:
- 蓝色区是
@Link(父子双方计数器同步) - 绿色区是
@Prop(父改子变,子改父不变)
代码笔记
// @State / @Link / @Prop 三者区别对比示例
//
// @State:父组件的数据源(状态)
// @Link :父子双向同步 —— 子改,父也变;父改,子也变
// @Prop :父→子单向传递 —— 父改子变,但子改父不变(子拿到的是副本)
@Entry
@Component
struct Index {
// 唯一数据源,只声明在父组件
@State linkCount: number = 0; // 给 @Link 用
@State propCount: number = 0; // 给 @Prop 用
build() {
Column({ space: 24 }) {
// ===== 标题 =====
Text('状态传递对比')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ top: 24 })
Text('点下方按钮观察:Link 子组件改动会同步到父,Prop 子组件改动不会。')
.fontSize(13)
.fontColor('#888')
.textAlign(TextAlign.Center)
.width('90%')
// ===== 父组件状态显示 =====
Column({ space: 8 }) {
Text('父组件 @State')
.fontSize(16)
.fontColor('#666')
Row({ space: 24 }) {
Column() {
Text('linkCount').fontSize(13).fontColor('#999')
Text(this.linkCount.toString())
.fontSize(32).fontWeight(FontWeight.Bold).fontColor('#007AFF')
}
Column() {
Text('propCount').fontSize(13).fontColor('#999')
Text(this.propCount.toString())
.fontSize(32).fontWeight(FontWeight.Bold).fontColor('#34C759')
}
}
}
.padding(16)
.backgroundColor('#F5F5F7')
.borderRadius(16)
.width('90%')
.alignItems(HorizontalAlign.Center)
// ===== 父组件操作按钮 =====
Row({ space: 16 }) {
Button('父: linkCount +1')
.backgroundColor('#007AFF')
.onClick(() => { this.linkCount++; })
Button('父: propCount +1')
.backgroundColor('#34C759')
.onClick(() => { this.propCount++; })
}
// ===== Link 子组件(双向) =====
LinkChild({ count: $linkCount })
// ===== Prop 子组件(单向) =====
PropChild({ count: this.propCount })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Center)
}
}
// ============== @Link 子组件:双向同步 ==============
@Component
struct LinkChild {
@Link count: number; // 接收父的引用,类型必须与父 @State 一致
build() {
Column({ space: 10 }) {
Row() {
Text('@Link 子组件').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#007AFF')
Blank()
Text('双向').fontSize(12).fontColor('#FFF').backgroundColor('#007AFF')
.borderRadius(8).padding({ left: 6, right: 6, top: 2, bottom: 2 })
}.width('100%')
Text('count = ' + this.count)
.fontSize(24).fontWeight(FontWeight.Bold).fontColor('#007AFF')
Button('子组件 +1(会同步给父)')
.backgroundColor('#007AFF')
.width('100%')
.onClick(() => { this.count++; })
}
.padding(16)
.backgroundColor('#EAF2FF')
.borderRadius(16)
.width('90%')
.alignItems(HorizontalAlign.Center)
}
}
// ============== @Prop 子组件:单向(父→子) ==============
@Component
struct PropChild {
@Prop count: number; // 接收父的副本,父改子变,子改父不变
build() {
Column({ space: 10 }) {
Row() {
Text('@Prop 子组件').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#34C759')
Blank()
Text('单向').fontSize(12).fontColor('#FFF').backgroundColor('#34C759')
.borderRadius(8).padding({ left: 6, right: 6, top: 2, bottom: 2 })
}.width('100%')
Text('count = ' + this.count)
.fontSize(24).fontWeight(FontWeight.Bold).fontColor('#34C759')
Button('子组件 +1(不会同步给父)')
.backgroundColor('#34C759')
.width('100%')
.onClick(() => { this.count++; })
}
.padding(16)
.backgroundColor('#E9F8EE')
.borderRadius(16)
.width('90%')
.alignItems(HorizontalAlign.Center)
}
}