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

鸡腿大王 8 阅读 1 评论 ArkTsArkUI

知识点讲解

一、为什么需要这些装饰器?

在 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)
父→子 ✅ 同步 ✅ 同步
子→父 ✅ 同步 ❌ 不同步
子能否给初值
支持对象/数组
典型场景 数据源头 父子共同操作同一数据 只读展示

七、什么时候用哪个?

三个判断问题:

  1. 数据归谁所有? → 归谁就用 @State 声明。
  2. 子组件需不需要改父的数据?
    • 需要 → 用 @Link(双向)
    • 不需要 → 用 @Prop(单向)
  3. 数据是对象/数组吗?
    • 是 → 必须 @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 })       // ✅
@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)
  }
}