当前位置:首页>鸿蒙APP>鸿蒙状态管理三兄弟:@State、@Prop、@Link到底怎么用?

鸿蒙状态管理三兄弟:@State、@Prop、@Link到底怎么用?

  • 2026-08-21 13:54:40
鸿蒙状态管理三兄弟:@State、@Prop、@Link到底怎么用?

上篇文章聊了ArkTS、ArkUI、Ability三者的关系,有读者在后台问:“代码写着写着,变量明明改了,UI就是不刷新。传了个值给子组件,子组件却改不了。到底哪出了问题?”

这些问题的本质都一样——状态管理的装饰器没选对。

ArkUI采用了MVVM模式,ViewModel将数据与视图绑定在一起,更新数据时直接更新视图。当自定义组件内变量被装饰器装饰时,就变成了状态变量,状态变量的改变会触发UI的渲染刷新。UI是状态的函数—— UI = f(state)。

状态管理不是语法技巧,而是组件通信的底层逻辑。下面把这套机制拆开讲透。

一、@State:组件自己的“记忆单元”

@State是最基础的装饰器,用于定义组件内部的状态变量,是组件的私有状态。它的生命周期与组件绑定,组件销毁时变量也随之销毁。

核心特征:

  • 变量只能从组件内部访问和修改

  • 变量变化时,触发当前组件及子组件的UI刷新

  • 声明时必须指定类型并本地初始化

典型场景:组件内部的私有状态——输入框内容、开关状态、Tab切换索引、计数器等。

@Componentstruct CounterDemo {  @State count: number = 0;  // 私有状态,初始值为0  build() {    Column() {      Text(`当前计数:${this.count}`)      Button('点我+1')        .onClick(() => {          this.count += 1;  // 本组件自己改,UI自动刷新        })    }  }}

@State 标记的count变量发生变化时,ArkUI框架会自动检测到状态变化,重新执行build()方法渲染最新视图。

二、@Prop:父组件给的“只读副本”

@Prop用于父组件向子组件传递数据,建立单向同步关系。

核心特征:

  • 父组件的数据源变化时,子组件的@Prop 变量同步更新

  • 子组件可以修改@Prop 变量,但变化不会同步回父组件

  • 当数据源更改时,会覆盖子组件本地的所有更改

简单说:父能改子,子改不了父。

@Componentstruct ChildTitle {  @Prop title: string;  // 从父组件接收,只读  build() {    Text(this.title)  }}@Entry@Componentstruct Parent {  @State parentTitle: string = '我是父组件传来的标题';  build() {    Column() {      ChildTitle({ title: this.parentTitle })      Button('改标题')        .onClick(() => {          this.parentTitle = '父组件改了标题';  // 父改,子跟着变        })    }  }}

类型要求:@Prop与数据源的类型必须相同。支持Object、class、string、number、boolean、enum及这些类型的数组,不支持any、联合类型、undefined和null。

性能注意:在组件复用场景中,@Prop深度嵌套数据建议不要超过5层,嵌套太深会导致深拷贝占用空间过大及垃圾回收开销,影响性能。

三、@Link:父子共享的“同一份数据”

@Link也用于父组件向子组件传递数据,但建立的是双向同步关系。

核心特征:

  • 子组件可以修改通过@Link接收的数据,修改会同步回父组件

  • 父子组件共享同一份数据,状态完全同步

  • @Link变量不能在子组件中直接初始化——它不是子组件自己的状态,而是与父组件状态变量建立双向同步关系的入口

简单说:父子都能改,改了都刷新。

@Componentstruct ChildCounter {  @Link count: number;  // 与父组件共享  build() {    Button(`子组件加1:${this.count}`)      .onClick(() => {        this.count++;  // 改的是父组件的count,父子同时更新      })  }}@Entry@Componentstruct ParentCounter {  @State count: number = 0;  build() {    Column() {      Text(`父组件显示:${this.count}`)      ChildCounter({ count: this.count })  // 传入状态变量    }  }}

@Link 能观察到的变化类型比@Prop更丰富    :boolean、string、number类型的变化,class或Object的赋值和属性赋值,array的添加、删除、更新单元,Date类型的整体赋值及通过set系列方法的属性更新。

四、三者的选择原则

搞清楚“谁拥有数据、谁能修改、改了刷不刷新UI”这三个问题,就不会再用错。

选择口诀: 自己用的用@State ,父给的只读用@Prop ,父子共享用@Link 。

更细化的判断逻辑:

  • 状态仅在组件内部使用、不需要与其他组件共享 →@State

  • 子组件只需要读取父组件的状态,不需要修改 →@Prop

  • 子组件需要修改父组件的状态 →@Link

五、一个容易被忽略的性能问题

@Prop 在不会改变子组件内状态变量值的情况下使用,会导致组件创建的耗时增加。如果子组件只是展示数据、没有任何修改意图,建议优先考虑普通参数传递而非@Prop。

另外,状态变量的管理是有开销的,应在合理场景使用。普通变量用状态变量标记会导致性能劣化。如果一个变量仅用于读取、从未被修改,不应该定义为状态变量。

六、深入一步:嵌套对象的监听局限

@State 有一个重要局限:它是一个“浅层”装饰器,只关心第一层,嵌套对象的属性变化它根本感知不到。

@Observedclass User {  name: string = '';  age: number = 0;}@Componentstruct Demo {  @State user: User = new User();  build() {    Column() {      Text(this.user.name)      Button('改名字')        .onClick(() => {          this.user.name = '新名字';  // ❌ UI不会刷新!          // @State只监听user本身的赋值,不监听user.name的变化        })    }  }}

嵌套场景下,单单使用@State 并不能监听到变量的状态变化。这时需要配合@Observed 和@ObjectLink 装饰器来解决。这个主题内容较多,下一篇单独展开。

回到最初的问题:变量改了UI不刷新、子组件改不了父组件的数据——大概率是装饰器选错了。

@State 管理自己的私有状态,@Prop 做父到子的单向传递,@Link 做父子双向共享。三者各司其职,选对了一个,状态管理的问题就解决了一大半。

最新文章

随机文章