上篇文章聊了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用于父组件向子组件传递数据,建立单向同步关系。
核心特征:
简单说:父能改子,子改不了父。
@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也用于父组件向子组件传递数据,但建立的是双向同步关系。
核心特征:
简单说:父子都能改,改了都刷新。
@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 。
更细化的判断逻辑:
五、一个容易被忽略的性能问题
@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 做父子双向共享。三者各司其职,选对了一个,状态管理的问题就解决了一大半。