被装饰器搞晕的不止你一个

我见过不少从其他框架转到 HarmonyOS6 PC 的开发者,第一反应都是:"这装饰器也太多了吧?"React 那边一个 useState 打天下,Vue 也就 ref 和 reactive 两个核心 API。到了 ArkUI 这里,@State@Prop@Link@Provide@Consume@Observed@ObjectLink@StorageLink@StorageProp@Watch@Builder@BuilderParam@Styles@Extend……一长串下来,光记住名字就够费劲的,更别说搞清楚什么时候该用哪个了。

但说实话,这些装饰器的设计是有内在逻辑的。每个装饰器解决的都是一个具体的问题场景。今天这篇文章就是帮你建立一套"决策框架"——给你一个场景,你能快速判断该用什么方案。

先建立全局认知:状态管理的几个维度

在开始逐个讲解之前,有必要先理清 ArkUI 状态管理的几个维度。你遇到的所有选型问题,都可以从这几个维度来缩小范围:

数据流向——是组件自己的状态?还是从父组件传下来的?还是要传给子组件?还是要跨层级共享?

绑定方式——是单向的(只读)?还是双向的(可修改并回传)?

数据粒度——是整个对象的变化?还是对象内部某个属性的变化?

生命周期——是跟随组件的?还是跟随应用的?还是需要持久化到磁盘的?

复用需求——是 UI 结构的复用?还是样式的复用?

想清楚这几个维度,选型基本就确定了。

决策树:一张图找到你要的装饰器

下面是我总结的决策流程。遇到一个状态管理需求时,按顺序问自己这些问题:

需求是什么?
│
├─ 组件内部的状态(不涉及父子传递)
│   └─ @State
│
├─ 从父组件接收数据
│   ├─ 只读展示,不需要修改 ──→ @Prop
│   ├─ 需要修改并同步回父组件 ──→ @Link
│   └─ 对象内部属性需要被观察 ──→ @ObjectLink(配合父组件 @Observed)
│
├─ 向子组件传递数据
│   ├─ 传递基本类型的值 ──→ @Prop
│   ├─ 传递并需要双向绑定 ──→ @Link
│   └─ 传递可观察对象 ──→ @Observed + @ObjectLink
│
├─ 跨层级共享(祖先到后代,跳过中间层)
│   ├─ 写入方 ──→ @Provide
│   └─ 读取方 ──→ @Consume
│
├─ 应用级全局状态
│   ├─ 需要双向绑定(组件可修改) ──→ @StorageLink
│   ├─ 只需要读取 ──→ @StorageProp
│   └─ 环境变量/系统信息 ──→ @StorageProp
│
├─ 监听状态变化
│   └─ @Watch(搭配 @State/@Prop/@Link 等)
│
├─ UI 结构复用
│   ├─ 组件内部的可复用片段 ──→ @Builder
│   └─ 支持外部传入自定义内容 ──→ @BuilderParam
│
└─ 样式复用
    ├─ 属性级别的样式复用 ──→ @Styles
    └─ 组件级别的样式扩展 ──→ @Extend

逐一拆解:每个装饰器的正确用法

@State:组件自己的小秘密

@State 是最基础、用得最多的装饰器。它声明的变量只属于当前组件,外界看不到也改不了。

适用场景:组件内部的 UI 状态——面板是否展开、输入框的当前值、动画的进度等。

@Component
struct ExpandablePanel {
  @State isExpanded: boolean = false
  @State inputText: string = ''

  build() {
    Column() {
      Row() {
        Text('高级选项').fontSize(14).layoutWeight(1)
        Text(this.isExpanded ? '收起' : '展开')
          .fontSize(12)
          .fontColor('#007DFF')
          .onClick(() => { this.isExpanded = !this.isExpanded })
      }
      .width('100%').padding(12)

      if (this.isExpanded) {
        TextInput({ placeholder: '请输入...', text: this.inputText })
          .width('100%')
          .onChange((value: string) => { this.inputText = value })
          .padding(12)
      }
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
  }
}

最佳实践@State 变量应该初始化为一个合理的默认值。框架会用初始值的类型来做类型推断,所以别用 nullundefined 做初始值。

常见错误:把本该用 @Prop 接收的数据用 @State 定义了,然后在 aboutToAppear 里从外部赋值。这种写法虽然能跑,但破坏了数据流的清晰性。

@Prop:父传子的单行道

@Prop 用于子组件接收父组件传来的数据。它是值传递——子组件拿到的是拷贝,修改不会影响父组件。

适用场景:纯展示型组件,比如头像、标签、卡片等。

@Component
struct TagView {
  @Prop label: string = ''
  @Prop color: string = '#007DFF'
  @Prop size: 'small' | 'large' = 'small'

  build() {
    Text(this.label)
      .fontSize(this.size === 'small' ? 10 : 13)
      .fontColor('#FFFFFF')
      .backgroundColor(this.color)
      .borderRadius(12)
      .padding({
        left: this.size === 'small' ? 8 : 12,
        right: this.size === 'small' ? 8 : 12,
        top: this.size === 'small' ? 2 : 4,
        bottom: this.size === 'small' ? 2 : 4
      })
  }
}

// 使用
TagView({ label: '新品', color: '#FF4D4F', size: 'large' })
TagView({ label: '限时', color: '#FAAD14' })

最佳实践@Prop 声明的变量一定要有默认值。因为父组件可能忘记传参,默认值是你的安全网。

注意@Prop 支持基本类型(string、number、boolean)和数组。对象类型也能传,但因为是值拷贝,大对象的拷贝会有性能开销。

@Link:父子之间的双向通道

@Link 也是父传子,但它是引用传递——子组件修改了值,父组件也会同步更新。

适用场景:子组件需要修改数据并通知父组件——比如一个开关组件、一个计数器组件。

@Component
struct CounterComponent {
  @Link count: number
  @Prop step: number = 1

  build() {
    Row({ space: 12 }) {
      Button('-')
        .width(32).height(32)
        .fontSize(18)
        .onClick(() => { this.count = Math.max(0, this.count - this.step) })

      Text(this.count.toString())
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .width(50)
        .textAlign(TextAlign.Center)

      Button('+')
        .width(32).height(32)
        .fontSize(18)
        .onClick(() => { this.count += this.step })
    }
  }
}

// 父组件中使用
@Entry
@Component
struct CounterPage {
  @State itemCount: number = 5

  build() {
    Column({ space: 16 }) {
      Text(`当前数量: ${this.itemCount}`).fontSize(16)
      CounterComponent({ count: $itemCount, step: 2 })
      // 注意用 $ 符号传递引用
    }
    .padding(16)
  }
}

最佳实践:用 $ 符号来传递 @Link 参数(如 $itemCount),这是 ArkUI 的语法约定。@Link 变量不需要设置默认值——它的值永远来自父组件。

和 @Prop 的选择:如果子组件只展示数据不修改 → @Prop;如果子组件需要修改数据 → @Link。简单粗暴但有效。

@Provide/@Consume:跨层级的数据共享

当数据需要在组件树中"穿透"好几层时,用 @Prop@Link 一层层传太痛苦了。@Provide@Consume 就是解决这个问题的。

适用场景:主题配置、用户信息、语言设置等全局性的、多个层级都需要用到的数据。

// 祖先组件
@Component
struct AppRoot {
  @Provide('theme') currentTheme: string = 'light'
  @Provide('user') userName: string = '开发者'

  build() {
    Column() {
      HeaderBar()
      ContentArea()
    }
  }
}

// 中间组件(不需要知道 theme 和 userName 的存在)
@Component
struct ContentArea {
  build() {
    Column() {
      ArticleList()
    }
  }
}

// 深层后代组件,直接使用 @Consume 读取
@Component
struct ArticleCard {
  @Consume('theme') currentTheme: string
  @Consume('user') userName: string

  build() {
    Column() {
      Text('文章标题')
        .fontColor(this.currentTheme === 'dark' ? '#FFFFFF' : '#333333')
      Text(`作者: ${this.userName}`)
        .fontSize(12)
        .fontColor('#999999')
    }
    .backgroundColor(this.currentTheme === 'dark' ? '#2C2C3A' : '#FFFFFF')
  }
}

最佳实践:给 @Provide@Consume 加上字符串别名(如 'theme'),避免依赖变量名匹配导致的脆弱绑定。如果中间某个组件也需要这个值,它可以同时 @Consume@Provide 同一个 key,实现"覆盖"效果。

和 @StorageLink 的区别@Provide/@Consume 是组件树内的共享,跟随组件的生命周期;@StorageLink 是应用级的,跟随应用的生命周期并且会持久化。

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

这对组合专门解决"对象内部属性变化不触发 UI 更新"的问题。在上一篇文章(任务管理应用)里我们详细讲了这对组合的用法。

适用场景:列表中每个条目是复杂对象、需要在子组件中修改对象属性。

@Observed
class UserInfo {
  name: string = ''
  age: number = 0
  email: string = ''
  isOnline: boolean = false
}

@Component
struct UserCard {
  @ObjectLink user: UserInfo

  build() {
    Row() {
      Column({ space: 4 }) {
        Text(this.user.name).fontSize(16).fontWeight(FontWeight.Bold)
        Text(this.user.email).fontSize(12).fontColor('#999999')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      // 点击切换在线状态,@ObjectLink 确保 UI 自动更新
      Column()
        .width(12).height(12)
        .borderRadius(6)
        .backgroundColor(this.user.isOnline ? '#52C41A' : '#D9D9D9')
        .onClick(() => { this.user.isOnline = !this.user.isOnline })
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
  }
}

最佳实践@Observed 标记的类应该保持结构简洁。@ObjectLink 变量不能在组件内部初始化,必须由父组件传入。如果父组件用 @State 管理 @Observed 对象的数组,添加和删除元素时需要创建新数组来触发列表刷新。

@StorageLink/@StorageProp:应用级持久化

这对组合直接连接 AppStorage,实现应用级别的状态共享和持久化。在设置页面那篇文章里我们用了大量的 @StorageLink

适用场景:用户设置、主题偏好、登录状态等需要在多个页面间共享且需要持久化的数据。

// 页面A:写入设置
@Component
struct SettingsView {
  @StorageLink('app_fontSize') fontSize: number = 16
  @StorageLink('app_theme') theme: string = 'light'

  build() {
    Column() {
      Slider({ value: $$this.fontSize, min: 12, max: 24, step: 1 })
      // fontSize 变化自动同步到 AppStorage
    }
  }
}

// 页面B:读取设置(只读)
@Component
struct ContentView {
  @StorageProp('app_fontSize') fontSize: number = 16
  @StorageProp('app_theme') theme: string = 'light'

  build() {
    Text('Hello World')
      .fontSize(this.fontSize)
      .fontColor(this.theme === 'dark' ? '#FFFFFF' : '#333333')
  }
}

最佳实践:需要修改全局状态的地方用 @StorageLink,只需要读取的地方用 @StorageProp。避免到处都用 @StorageLink,那样任何一个组件都可能意外修改全局状态,排查问题很头疼。

@Watch:变化监听器

@Watch 不是一个独立的状态装饰器,而是搭配其他装饰器使用的"增强器"。它让你在某个状态变量变化时执行自定义逻辑。

适用场景:表单联动计算、数据校验、变化日志、动画触发等。

@Component
struct FormValidator {
  @State @Watch('validateEmail') email: string = ''
  @State emailError: string = ''
  @State emailValid: boolean = false

  validateEmail(propName: string): void {
    if (this.email.length === 0) {
      this.emailError = ''
      this.emailValid = false
      return
    }
    let regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    if (regex.test(this.email)) {
      this.emailError = ''
      this.emailValid = true
    } else {
      this.emailError = '邮箱格式不正确'
      this.emailValid = false
    }
  }

  build() {
    Column({ space: 4 }) {
      TextInput({ placeholder: '请输入邮箱', text: this.email })
        .onChange((value: string) => { this.email = value })
        .borderColor(this.emailError.length > 0 ? '#FF4D4F' : '#E0E0E0')

      if (this.emailError.length > 0) {
        Text(this.emailError).fontSize(11).fontColor('#FF4D4F')
      }
      if (this.emailValid) {
        Text('格式正确').fontSize(11).fontColor('#52C41A')
      }
    }
    .alignItems(HorizontalAlign.Start)
  }
}

最佳实践@Watch 回调里避免修改触发它的那个变量(会导致循环)。多个变量需要联动计算时,用一个统一的回调方法。

@Builder/@BuilderParam:UI 结构复用

@Builder 用于封装可复用的 UI 片段,@BuilderParam 用于让组件接受外部传入的 Builder 作为"插槽"。

适用场景:重复出现的 UI 模式(列表项、分隔线、标题栏等),以及需要支持自定义内容的容器组件。

@Builder
function SectionHeader(title: string, subtitle?: string) {
  Column({ space: 2 }) {
    Text(title).fontSize(16).fontWeight(FontWeight.Bold)
    if (subtitle) {
      Text(subtitle).fontSize(12).fontColor('#999999')
    }
  }
  .width('100%')
  .alignItems(HorizontalAlign.Start)
  .padding({ bottom: 8 })
}

// 可接受外部内容的容器组件
@Component
struct CardContainer {
  @Prop title: string = ''
  @BuilderParam content: () => void = this.defaultContent

  @Builder
  defaultContent() {
    Text('暂无内容').fontSize(14).fontColor('#999999')
  }

  build() {
    Column({ space: 8 }) {
      SectionHeader(this.title)
      this.content()
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .padding(16)
  }
}

// 使用时传入自定义内容
CardContainer({
  title: '今日推荐',
  content: () => {
    Column({ space: 8 }) {
      Text('推荐内容1').fontSize(14)
      Text('推荐内容2').fontSize(14)
    }
  }
})

@Styles/@Extend:样式层面的复用

@Styles 复用一组样式属性,@Extend 在系统组件的样式基础上做扩展。

适用场景:多个组件共享同一套样式(卡片样式、按钮样式、输入框样式等)。

@Styles
function cardStyle() {
  .backgroundColor('#FFFFFF')
  .borderRadius(12)
  .padding(16)
  .shadow({ radius: 4, color: '#0A000000', offsetX: 0, offsetY: 2 })
}

@Styles
function primaryButtonStyle() {
  .backgroundColor('#007DFF')
  .borderRadius(8)
  .fontColor('#FFFFFF')
  .fontSize(14)
  .fontWeight(FontWeight.Medium)
}

// @Extend 扩展系统组件
@Extend(Button)
function dangerButton() {
  .backgroundColor('#FF4D4F')
  .fontColor('#FFFFFF')
  .borderRadius(8)
}

// 使用
Column() {
  Text('卡片内容').cardStyle()
  Button('主要操作').primaryButtonStyle()
  Button('危险操作').dangerButton()
}

@Styles 和 @Extend 的区别@Styles 是通用的样式集合,可以用在任何组件上;@Extend 是针对某个特定系统组件的样式扩展,只能用在对应的组件类型上。

场景化的最佳实践汇总

场景一:电商商品详情页

商品详情页的数据来源多、层级深。典型的状态管理方案:

  • 商品信息(名称、价格、图片)→ @Prop 传递给各子组件展示
  • 购买数量 → @Link 实现计数器和总价组件的双向绑定
  • 用户已选规格 → @Provide 让多个 Tab 页共享
  • 价格和库存的实时计算 → @Watch 监听规格变化触发重算
  • 用户偏好(字号、主题)→ @StorageProp 全局读取

场景二:社交应用的消息列表

  • 消息数据 → @Observed 标记 Message 类
  • 消息卡片组件 → @ObjectLink 接收,支持"已读"状态的修改
  • 未读数统计 → @Provide 让头部导航栏和消息列表共享
  • 消息气泡的不同样式 → @Builder 封装不同类型的消息 UI
  • 深色模式 → @StorageProp 读取主题配置

场景三:表单填写页

  • 表单各字段 → @State 管理每个输入值
  • 字段校验 → @Watch 监听输入变化执行校验逻辑
  • 提交按钮状态 → 基于校验结果计算
  • 表单模板(标题+输入框+错误提示)→ @Builder 复用
  • 用户自动填充的历史记录 → @StorageLink 持久化

容易踩的几个坑

坑一:@State 对象属性变化不触发更新

这大概是 HarmonyOS6 PC 开发者被问得最多的问题了。@State 只跟踪变量的引用变化,不跟踪对象内部属性。如果你 this.obj.name = 'new',UI 不会更新。

解决方案:用 @Observed 标记类,子组件用 @ObjectLink 接收。或者手动触发:this.obj = { ...this.obj, name: 'new' }

坑二:@Link 没加 $ 符号

父组件传参时忘记加 $(比如写成 count: this.count 而不是 count: $count),结果是传了值而不是引用。子组件用 @Link 接收时会报错或者行为异常。

坑三:@Provide 的 key 冲突

如果两个祖先组件都 @Provide 了同名的 key,后代 @Consume 时会拿到哪个?答案是最近的祖先。但这种隐式的覆盖很容易造成 bug。建议用唯一的 key 名,比如 provide_settings_theme

坑四:数组的不可变更新

直接 this.array.push(item) 不会触发 UI 更新。正确的写法是 this.array = [...this.array, item]。删除也一样:this.array = this.array.filter(...) 而不是 splice

坑五:@Builder 里访问不到 this

全局的 @Builder 函数(不在组件内定义的)没有 this 上下文。如果你需要在 Builder 里访问组件状态,必须把 Builder 定义在组件内部,或者通过参数传递。

一张总结表

装饰器 数据流向 绑定方式 适用粒度 持久化
@State 组件内部 双向 基本类型/对象
@Prop 父→子 单向 基本类型/数组
@Link 父↔子 双向 基本类型/对象
@Provide 祖先→后代 双向 基本类型/对象
@Consume 祖先→后代 双向 基本类型/对象
@Observed - -
@ObjectLink 父→子 双向(属性级) @Observed对象
@StorageLink AppStorage↔组件 双向 基本类型/对象
@StorageProp AppStorage→组件 单向 基本类型/对象
@Watch - 监听 搭配其他使用 -
@Builder - - UI片段 -
@BuilderParam 外部→组件 插槽 UI片段 -
@Styles - - 属性样式 -
@Extend - - 组件样式 -

小结

状态管理选型的核心在于搞清楚数据的流向变化的方式。组件内部用 @State,父传子看是否需要回写来决定 @Prop@Link,对象深层变化用 @Observed/@ObjectLink,跨层级用 @Provide/@Consume,全局持久化用 @StorageLink/@StorageProp

不需要一口气记住所有装饰器。把这篇文章当个速查手册,遇到具体场景时回来对照决策树,几次之后你就能形成直觉了。HarmonyOS6 PC 的状态管理体系虽然看起来复杂,但每个装饰器都有明确的职责边界。搞清楚了这些边界,写出来的代码自然就干净了。

Logo

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

更多推荐