哈喽,兄弟们,我是 V 哥!

最近有粉丝在群里发了个截图,代码里密密麻麻全是 @State,看得我密集恐惧症都犯了。他说:“V 哥,我的 App 怎么越改越卡?明明只是改了列表里的一个文字,整个页面都在闪烁刷新!”

不看不知道,一看吓一跳!好家伙,子组件里用 @State 接父组件的数据,深层对象直接修改属性,数据一层一层往下传……

兄弟们,这哪是写代码,这简直是给鸿蒙的渲染引擎**“下毒”**!在 API 21 的严格模式下,状态管理是道送命题。用不对,不仅逻辑乱,性能更是灾难。

今天 V 哥就拿出压箱底的**“状态管理三板斧”**,帮你理清 ArkTS 的状态脉络。这文章读完,起码能帮你省下 3 天找 Bug 和掉头发的时间!


坑点一:子组件乱用 @State,导致“过度渲染”

🔴 错误示范(千万别这么写!)

很多兄弟觉得,数据变了 UI 就要变,那就加个 @State 嘛!

// 错误代码示例
@Component
struct ChildView {
  @State count: number = 0; // ❌ 灾难的开始!

  build() {
    Text(this.count.toString())
  }
}

问题在哪?
你在父组件里给 ChildView 传了个 count。一旦父组件刷新,哪怕这个 count 没变,或者只是父组件的其他状态变了,这个 ChildView 因为有 @State,它就会觉得“我有独立状态,我得重新初始化”,导致不必要的重绘。

✅ V 哥的正解:只读数据用 @Prop

如果子组件只是展示数据,数据源在父组件里,那子组件必须用 @Prop@Prop 是单向同步,父变了子才变,它不会触发额外的初始化开销。

@Component
struct ChildView {
  // ✅ 修复:使用 @Prop 接收父组件数据
  // @Prop 是只读的,不能在子组件里直接 this.count++
  @Prop count: number = 0; 

  build() {
    Text(`V哥计数: ${this.count}`)
      .fontSize(20)
  }
}

@Entry
@Component
struct PropDemo {
  // 数据源头在父组件
  @State total: number = 0;

  build() {
    Column() {
      ChildView({ count: this.total })
      
      Button('点我增加')
        .onClick(() => {
          this.total++;
        })
    }
  }
}

坑点二:深层对象属性变了,UI 死活不刷新

🔴 痛点直击

这绝对是鸿蒙开发里头号“玄学”Bug

你有一个 User 对象,@State user: User。你点击按钮修改了 user.age。日志里打印出来 age 确实变了,但界面上的数字就是纹丝不动!

class User {
  name: string = 'V哥';
  age: number = 18;
}
// ...
this.user.age = 19; // ❌ UI 不会刷新!

🔍 原理剖析

ArkTS 的 @State 观察机制,默认只观察对象的引用(地址)。你修改了对象内部的属性,对象地址没变,系统就会认为:“咦?地址没变,那就不用刷新 UI 了。” 于是它就“偷懒”了。

✅ V 哥的正解:API 21 王炸组合 —— @Observed + @ObjectLink

在 API 21 中,处理嵌套对象或深层修改,必须使用嵌套类观察机制。这是解决复杂对象状态管理的终极方案。

兄弟们,下面这段代码是核心中的核心,建议直接复制到 DevEco Studio 6.0 跑一遍,理解透彻了,状态管理你就通关了。

/**
 * V哥实战案例:深层对象状态同步
 * 场景:修改用户资料的某个属性,UI 自动刷新
 */

// 第一步:被观察的类
// 注意:@Observed 装饰类,这是对象能被深层观察的前提
@Observed
class Address {
  city: string = '深圳市';
  zipCode: string = '518000';
}

// 第二步:被观察的类
// 注意:如果这个类里有其他对象(如 Address),那个对象类也必须加 @Observed
@Observed
class User {
  name: string = 'V哥';
  age: number = 18;
  address: Address = new Address(); // 嵌套对象
}

@Entry
@Component
struct ObjectLinkDemo {
  // 第三步:父组件持有状态
  // 这里的 User 对象包含了深层属性
  @State currentUser: User = new User();

  build() {
    Column() {
      Text('V哥的状态管理实验室')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      // 第四步:子组件中使用 @ObjectLink
      // @ObjectLink 接收的是对象实例,它会建立起与父组件对象的双向监听
      UserCard({ user: this.currentUser })
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

// 第五步:子组件
@Component
struct UserCard {
  // ✅ 关键点:@ObjectLink
  // 它能感知到 user 对象内部任何属性的变化!
  @ObjectLink user: User;

  build() {
    Column() {
      Text(`姓名: ${this.user.name}`)
        .fontSize(18)

      Text(`年龄: ${this.user.age}`)
        .fontSize(18)
        .margin({ top: 5 })

      Text(`城市: ${this.user.address.city}`)
        .fontSize(18)
        .fontColor(Color.Red)
        .margin({ top: 5 })

      Divider()

      // 修改深层属性
      Button('修改城市(深层属性)')
        .width('100%')
        .margin({ top: 10 })
        .onClick(() => {
          // ✅ 修改嵌套对象的属性
          // 如果没用 @Observed 和 @ObjectLink,这里改了界面也不会动!
          this.user.address.city = '北京市';
          console.info("V哥日志:城市已修改为北京");
        })

      // 修改第一层属性
      Button('修改年龄(第一层属性)')
        .width('100%')
        .margin({ top: 10 })
        .onClick(() => {
          // ✅ 修改普通属性
          this.user.age++;
        })
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#F1F3F5')
    .borderRadius(12)
  }
}

V 哥划重点(背诵版):

  1. 类定义必须加 @Observed(无论是父类还是嵌套的子类)。
  2. 子组件接收对象必须用 @ObjectLink(不能用 @Prop)。
  3. 父组件依然用 @State 持有最初的那个对象引用。

坑点三:爷爷给孙子传数据,传到怀疑人生

🔴 痛点直击

假设你的组件层级是:GrandPa -> Father -> Son
如果 Son 需要 GrandPa 里的一个数据,你得先传给 FatherFather 再传给 Son
中间如果经过了 5 层组件,那代码写起来简直是灾难,中间层根本不需要这个数据,却得被迫定义变量接收。

✅ V 哥的正解:@Provide 和 @Consume

这就好比家里的长辈(GrandPa)把钱放到了客厅的保险箱里(@Provide),所有家庭成员(@Consume)都可以直接去拿,不需要一层层转交。

@Entry
@Component
struct GrandPaView {
  // ✅ 爷爷提供了数据
  // 这就像是一个“全局广播”,只要名字叫 'familyName',谁都能收得到
  @Provide('familyName') familyName: string = 'V哥全家桶';

  build() {
    Column() {
      Text('爷爷的页面')
        .fontSize(20)
        .margin(10)

      FatherView()
    }
  }
}

@Component
struct FatherView {
  build() {
    Column() {
      Text('爸爸的页面')
        .fontSize(18)
        .fontColor(Color.Gray)
      
      // 爸爸根本不需要知道 familyName 是啥,直接往下传
      SonView()
    }
  }
}

@Component
struct SonView {
  // ✅ 孙子直接消费数据
  // 只要这里的名字 'familyName' 和 @Provide 里的一样,就能接收到
  @Consume('familyName') familyName: string;

  build() {
    Text(`孙子拿到了: ${this.familyName}`)
      .fontSize(22)
      .fontWeight(FontWeight.Bold)
      .fontColor(Color.Orange)
      .margin(10)
  }
}

V 哥使用场景建议:
这招特别适合全局主题色用户登录信息全局配置这种贯穿整个 App 的数据。


小结一下

兄弟们,API 21 的状态管理其实很有逻辑,别乱用就行。

  1. 子组件只读展示?@Prop,别贪懒用 @State
  2. 深层对象要修改? 类加 @Observed,子组件加 @ObjectLink,这是正解。
  3. 跨层级传数据? 别傻傻地一层层传,用 @Provide@Consume

记住 V 哥这三招,你的代码不仅逻辑清晰,性能也能提升一大截。别再为了那个“改了不刷新”的 Bug 抓掉头发了,赶紧去重构吧!

我是 V 哥,咱们下期技术复盘见!👋

Logo

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

更多推荐