前言

刚学 ArkUI 的时候,状态管理那几个装饰器把我搞懵了——@State、@Prop、@Link,长得像亲兄弟,用起来完全不一样。照着官方文档抄了一遍代码,数据就是不同步,debug 了两个小时才发现用错了装饰器。

A hand-drawn doodle illustration on pure white pap

选错装饰器,轻则数据不同步,重则整个组件不刷新。

今天把这三个装饰器掰碎了讲,看完你就知道每个场景该用谁。

为什么状态管理这么头疼

ArkUI 是声明式框架,UI 是状态的函数:状态变了,UI 自动更新。但问题来了——

  • 父组件的数据,子组件能不能改?
  • 改了之后父组件知不知道?
  • 孙组件要访问祖先的数据怎么办?

这些问题的答案,就藏在装饰器的选择里。选对了,数据流通顺;选错了,刷新丢失、数据不同步,满屏 bug。

三种装饰器对比

先把最核心的区别摆出来:

维度 @State @Prop @Link
数据流向 组件内部,自给自足 父 → 子(单向) 父 ↔ 子(双向)
能否修改 随便改 能改,但不通知父组件 能改,同步通知父组件
类型限制 支持所有类型 只支持简单类型(string/number/boolean) 只支持简单类型
父组件传参 不需要 子组件({ prop: this.xxx }) 子组件({ link: $xxx })

A hand-drawn doodle illustration on pure white pap

| 性能影响 | 低 | 低 | 稍高(双向绑定有监听开销) |
| 使用场景 | 组件私有状态 | 展示型子组件 | 表单、开关等需回传的子组件 |

划重点:@Prop 改了不通知爹,@Link 改了会通知爹。 就这一句话的区别。

场景一:父子单向传递(@Prop)

父组件有个用户名,子组件只负责展示,不需要回传。

@Component
struct ParentA {
  @State username: string = '鸿蒙开发者'

  build() {
    Column() {
      Text('父组件:' + this.username)
        .fontSize(20)
      
      ChildA({ name: this.username })
    }
  }
}

@Component
struct ChildA {
  @Prop name: string

  build() {
    Text('子组件展示:' + this.name)
      .fontSize(16)
      .fontColor('#666')
  }
}

A hand-drawn doodle illustration on pure white pap

关键代码讲解:

  • @Prop name: string —— 子组件用 @Prop 接收,类型必须和父组件传的一致
  • ChildA({ name: this.username }) —— 父组件通过构造参数传递,注意不是 $ 语法
  • 父组件修改 username,子组件会跟着更新;但子组件改 name,父组件的 username 不变

这是"只读展示"场景的标准用法。 子组件像是拿了一份复印件,随便画,原件不受影响。

场景二:父子双向同步(@Link)

子组件是个开关,用户拨动开关后,父组件的状态也要同步更新。

@Component
struct ParentB {
  @State isDarkMode: boolean = false

  build() {
    Column() {
      Text('当前模式:' + (this.isDarkMode ? '深色' : '浅色'))
        .fontSize(20)
      
      ToggleChild({ mode: $isDarkMode })
    }
  }
}

@Component
struct ToggleChild {
  @Link mode: boolean

  build() {
    Toggle({ type: ToggleType.Switch, isOn: this.mode })
      .onChange((value: boolean) => {
        this.mode = value
      })
  }
}

关键代码讲解:

  • @Link mode: boolean —— 子组件用 @Link 声明,建立双向绑定
  • ToggleChild({ mode: $isDarkMode }) —— 注意 $ 符号,这是 @Link 传参的固定语法
  • this.mode = value —— 子组件修改 @Link 变量,父组件的 isDarkMode 同步更新

$ 语法就是告诉框架:这不是传值,是传引用,两边绑定了。

场景三:跨层级传递(@Provide + @Consume)

爷组件要给孙组件传数据,中间隔了一层,用 @Prop/@Link 得一层层透传,太恶心了。HarmonyOS7 提供了 @Provide@Consume 解决这个问题。

@Component
struct Grandparent {
  @Provide theme: string = 'blue'

  build() {
    Column() {
      Text('爷爷的主题色:' + this.theme)
      MiddleLayer()
    }
  }
}

@Component
struct MiddleLayer {
  build() {
    Column() {
      Text('中间层,我不关心主题')
      Grandchild()
    }
  }
}

@Component
struct Grandchild {
  @Consume theme: string

  build() {
    Text('孙子收到的主题色:' + this.theme)
      .fontColor(this.theme === 'blue' ? '#1890ff' : '#52c41a')
  }
}

关键代码讲解:

  • @Provide theme —— 祖先组件提供数据,自动向下广播
  • @Consume theme —— 后代组件消费数据,按变量名匹配,不需要中间层转发
  • 中间层 MiddleLayer 完全不需要知道 theme 的存在

这招在大型项目中特别好用。 全局主题、用户信息这类"到处都要用"的数据,用 @Provide/@Consume 最省心。

场景四:列表中的状态管理

列表里每个 item 都有独立状态(比如选中态),这是最容易出 bug 的地方。

@Component
struct ListDemo {
  @State items: SelectItem[] = [
    { id: 1, name: 'ArkTS', selected: false },
    { id: 2, name: 'ArkUI', selected: false },
    { id: 3, name: 'Flex', selected: true }
  ]

  build() {
    List() {
      ForEach(this.items, (item: SelectItem) => {
        ListItem() {
          ItemRow({ 
            name: item.name, 
            selected: item.selected,
            onToggle: () => {
              item.selected = !item.selected
            }
          })
        }
      }, (item: SelectItem) => item.id.toString())
    }
  }
}

@Component
struct ItemRow {
  @Prop name: string
  @Prop selected: boolean
  onToggle: () => void = () => {}

  build() {
    Row() {
      Text(this.name)
      Checkbox().select(this.selected).onChange(() => this.onToggle())
    }
  }
}

关键代码讲解:

  • 列表项状态通过 @Prop 传入,单项选中状态用 @Prop 就够了
  • 回调函数 onToggle 在父组件中修改 @State items,触发列表刷新
  • ForEach 的第三个参数 (item) => item.id.toString()键值生成器,别漏了,否则列表更新会出问题

选型决策树

一张图帮你快速决策:

需要状态管理?
│
├─ 组件自己用,不需要传给子组件?
│   └─ ✅ @State
│
├─ 传给子组件,子组件只展示不改?
│   └─ ✅ @Prop
│
├─ 传给子组件,子组件改了要同步回来?
│   └─ ✅ @Link
│
├─ 跨多层组件传递?
│   └─ ✅ @Provide + @Consume
│
└─ 列表中的单项状态?
    └─ ✅ @State(父) + @Prop(子) + 回调

写在最后

状态管理就一句话:数据谁拥有,修改权就归谁。 子组件要改父组件的数据,要么用 @Link 双向绑定,要么用回调函数通知父组件自己改。别偷懒直接改 @Prop,改了也白改。

记住这个原则,状态管理的 90% 的坑都能避开。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐