引子:状态管理是 ArkUI 的命脉

写鸿蒙应用,90% 的 bug 都和状态管理有关——数据变了 UI 没更新、子组件拿不到父组件的值、对象属性改了但页面没刷新。ArkUI 提供了六种状态管理工具,每种解决不同的问题。选错了,代码会越来越乱;选对了,数据流清晰,bug 自然少。

今天就从实际开发场景出发,拆解 @State、@Prop、@Link、@Provide/@Consume、@Observed/@ObjectLink、@Watch 六个装饰器的用法、区别和选型逻辑。
完整效果
在这里插入图片描述

先搞懂一个问题:状态从哪来

ArkUI 是声明式 UI——UI 是状态的函数:UI = f(state)。状态变了,UI 自动更新。

但"状态存在哪里"是个问题:

  • 只在当前组件用?→ 存组件内部
  • 父子组件共享?→ 存父组件,传给子组件
  • 祖孙多层传递?→ 存全局,跨层传递
  • 对象的某个属性变了?→ 需要深度观察

六种装饰器对应四种场景,搞清楚场景就能选对工具。

@State:组件内部的状态

@Component
struct Counter {
  @State count: number = 0;

  build() {
    Column() {
      Text(`点击了 ${this.count}`)
      Button('+1').onClick(() => { this.count++; })
    }
  }
}

在这里插入图片描述

作用域

@State 变量只在当前组件内有效。外部无法直接修改。

触发更新的条件

  • 基本类型(number、string、boolean):值变化就触发
  • 对象/数组:引用变化才触发(this.obj = newObj),属性变化不触发

常见错误

// 错误:修改对象属性,UI 不更新
@State user: UserInfo = { name: '张三', age: 25 };
this.user.age = 26;  // UI 不刷新!

// 正确:替换整个对象
this.user = { ...this.user, age: 26 };  // UI 刷新

适用场景

组件内部的私有状态——计数器、开关、输入框的值、当前选中的 Tab。

@Prop:父传子的单向绑定

// 父组件
@Component
struct Parent {
  @State message: string = 'hello';

  build() {
    Column() {
      Child({ msg: this.message })
      Button('改消息').onClick(() => { this.message = 'world'; })
    }
  }
}

// 子组件
@Component
struct Child {
  @Prop msg: string;

  build() {
    Text(this.msg)
  }
}

数据流

Parent.message → Child.msg(单向:父→子)

父组件的 message 变了,子组件的 msg 自动更新。但子组件不能反过来修改父组件的 message

@Prop 的拷贝语义

@Prop 会拷贝父组件的值。子组件拿到的是副本,修改副本不影响父组件。

// 子组件
@Prop msg: string;
this.msg = 'changed';  // 只改自己,父组件不受影响

适用场景

父组件传配置给子组件——标题文字、是否显示、颜色值。子组件只负责展示,不负责修改。

@Link:父子双向绑定

// 父组件
@Component
struct Parent {
  @State count: number = 0;

  build() {
    Column() {
      Child({ count: $count })  // 用 $ 传引用
      Text(`父组件: ${this.count}`)
    }
  }
}

// 子组件
@Component
struct Child {
  @Link count: number;

  build() {
    Button(`子组件 +1`).onClick(() => { this.count++; })
  }
}

数据流

Parent.count ↔ Child.count(双向:父↔子)

子组件修改 count,父组件的 count 也会变。两边同步。

$ 符号的含义

Child({ count: $count })  // 传引用
Child({ count: this.count })  // 传值(错误!)

$count 传的是变量的引用,this.count 传的是值。用错了,双向绑定就失效。

@Link vs @Prop

特性 @Prop @Link
数据流 父→子(单向) 父↔子(双向)
子组件修改 不影响父组件 同步到父组件
传值方式 msg: this.value count: $value

适用场景

子组件需要修改父组件的状态——表单输入、开关切换、列表项的选中状态。

@Provide/@Consume:跨层传递

// 祖先组件
@Component
struct Grandparent {
  @Provide('theme') currentTheme: string = 'dark';

  build() {
    Column() {
      Parent()
      Button('切换主题').onClick(() => {
        this.currentTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
      })
    }
  }
}

// 中间组件(不需要传递 theme)
@Component
struct Parent {
  build() { Child() }
}

// 后代组件
@Component
struct Child {
  @Consume('theme') currentTheme: string;

  build() {
    Text(`当前主题: ${this.currentTheme}`)
  }
}

数据流

Grandparent.currentTheme → Child.currentTheme(跨层:祖先→后代)

中间的 Parent 组件不需要知道 theme 的存在,数据直接穿透传递。

为什么需要 key

@Provide('theme') currentTheme: string;
@Consume('theme') currentTheme: string;

'theme' 是 key,@Provide@Consume 必须用同一个 key 才能匹配。一个组件可以 Provide 多个值,用不同的 key 区分。

适用场景

全局配置——主题、语言、用户信息、登录状态。不需要层层传递,直接跨层获取。

@Observed/@ObjectLink:对象深度观察

@Observed
class UserInfo {
  name: string = '';
  age: number = 0;
}

// 父组件
@Component
struct Parent {
  @State user: UserInfo = new UserInfo();

  build() {
    Column() {
      Child({ user: this.user })
      Button('改年龄').onClick(() => { this.user.age++; })
    }
  }
}

// 子组件
@Component
struct Child {
  @ObjectLink user: UserInfo;

  build() {
    Text(`${this.user.name} ${this.user.age}`)
  }
}

@Observed 的作用

给类加 @Observed,框架会深度观察这个类的实例。当实例的属性变化时,引用这个实例的组件会更新。

@ObjectLink 的作用

子组件用 @ObjectLink 接收 @Observed 对象,就能观察到属性的变化。

@State + @Observed 的组合

// 父组件用 @State + @Observed 类
@State user: UserInfo = new UserInfo();

// 子组件用 @ObjectLink
@ObjectLink user: UserInfo;

父组件修改 this.user.age,子组件会更新。不需要替换整个对象。

为什么 @State 单独不行

@State 只观察引用变化,不观察属性变化。如果 UserInfo 没加 @Observedthis.user.age++ 不会触发子组件更新。

@Observed + @ObjectLink vs @State

场景 用 @State 用 @Observed + @ObjectLink
基本类型 不需要
对象替换 不需要
对象属性修改
父子共享对象

适用场景

列表项的内部状态——每个列表项是一个对象,修改某个项的属性(比如收藏、展开),其他项不受影响。

@Watch:监听状态变化

@Component
struct SearchBox {
  @State @Watch('onKeywordChange') keyword: string = '';

  onKeywordChange(): void {
    console.log(`关键词变了: ${this.keyword}`);
    // 触发搜索请求
  }

  build() {
    TextInput({ text: this.keyword })
      .onChange((v: string) => { this.keyword = v; })
  }
}

@Watch 的触发时机

状态值变化时,自动调用 @Watch 指定的函数。在函数里可以做副作用——发请求、打日志、更新其他状态。

为什么不在 onChange 里直接处理

// 方案一:在 onChange 里处理
.onChange((v: string) => {
  this.keyword = v;
  this.doSearch(v);  // 需要手动调用
})

// 方案二:用 @Watch
@State @Watch('onKeywordChange') keyword: string = '';
// 只要 keyword 变了,自动触发,不管从哪里改的

@Watch 的好处是"不管状态从哪里被修改,都会触发"。如果以后有其他地方修改 keyword(比如清空按钮),不需要重复写处理逻辑。

适用场景

搜索防抖、表单校验、日志记录、状态同步。

选型决策树

需要管理状态?
├─ 只在组件内部用 → @State
├─ 父子组件共享
│   ├─ 子组件只读 → @Prop
│   └─ 子组件要改 → @Link
├─ 跨多层传递 → @Provide/@Consume
├─ 对象属性要深度观察 → @Observed + @ObjectLink
└─ 状态变化要触发副作用 → + @Watch

六种装饰器的对比表

装饰器 作用域 数据流 修改权 适用场景
@State 组件内 组件内 组件内 私有状态
@Prop 父→子 单向 子可改(不影响父) 配置传递
@Link 父↔子 双向 双方都可改 表单输入
@Provide 祖先→后代 跨层 祖先改,后代读 全局配置
@Consume 后代读祖先 跨层 后代读,祖先改 全局配置
@Observed 对象属性 深度观察 任何持有引用的组件 列表项状态
@ObjectLink 子组件读对象 深度观察 子组件可改 列表项状态
@Watch 任意 监听变化 触发回调 副作用处理

踩坑记录

坑 1:@Link 传值用了 this 而不是 $

// 错误
Child({ count: this.count });
// 正确
Child({ count: $count });

坑 2:@Observed 漏加

// 错误:UserInfo 没加 @Observed
class UserInfo { name: string = ''; }

// 正确
@Observed
class UserInfo { name: string = ''; }

坑 3:@Provide 和 @Consume 的 key 不一致

// 祖先
@Provide('theme') currentTheme: string;
// 后代
@Consume('color') currentTheme: string;  // key 是 'color',匹配不上!

坑 4:@Prop 没有默认值

// 错误:@Prop 没有默认值,编译报错
@Prop msg: string;

// 正确:给默认值
@Prop msg: string = '';

坑 5:@Watch 的函数名拼错

@State @Watch('onChage') keyword: string = '';  // 函数名拼错,不会报错但不会触发

坑 6:@State 数组的更新

// 错误:push 不触发更新
this.items.push(newItem);

// 正确:用展开运算符
this.items = [...this.items, newItem];

代码改进建议

1. 状态下沉

把状态放在"需要它的最小组�件"里。如果只有子组件用,不要放父组件。

2. 避免过度使用 @Provide

@Provide 是全局状态,用多了会让数据流难以追踪。优先用 @Prop/@Link 传参。

3. 用 @Watch 替代多处 onChange

如果一个状态变化需要在多个地方响应,用 @Watch 集中处理,比分散在各处的 onChange 更好维护。

4. @Observed 对象的不可变更新

虽然 @Observed 支持属性修改,但用不可变更新(替换对象)更安全,不容易出 bug。

5. 状态管理的测试

每个 @State 变量都应该有对应的测试用例,验证状态变化是否触发预期的 UI 更新。

总结

ArkUI 的六种状态管理工具解决四种场景:组件内部(@State)、父子传递(@Prop/@Link)、跨层传递(@Provide/@Consume)、对象深度观察(@Observed/@ObjectLink)。@Watch 是通用的副作用触发器,可以搭配任何装饰器使用。

适用边界:这个部分适合用作 ArkUI 状态管理的系统性学习,涵盖了六个装饰器的用法、区别、选型、踩坑和最佳实践。但如果要深入某个具体装饰器(比如 @Observed 的实现原理、@Provide 的依赖注入机制),还需要更底层的分析。

说白了,状态管理就是"把数据放在对的地方"。放在对的地方,UI 自动更新;放在错的地方,bug 满天飞。

Logo

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

更多推荐