我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言

老实讲,第一次用 ArkTS 做界面,我也干过“哪里不刷新就到处加装饰器”的事儿——结果页面是刷新了,性能也跟着“焕新”了(向下)。这篇我们把 @State / @Prop / @Link / @Provide / @Consume / @Observed / @ObjectLink / @Watch 等常用装饰器的边界、传参语义、可观察性陷阱讲清楚,再用一套可运行的微型样例收尾:同一数据在父子间同步、只读传参、复杂集合正确刷新、状态提升与局部重绘。不鸡汤,都是踩过的坑🤏。

目录(可当速查清单)

  • 装饰器一张表:职责与适用场景
  • 刷新边界:谁会触发重建、重绘、谁又不会
  • 父子同步与只读传参:@Prop vs @Link 的选择题
  • 复杂对象/集合陷阱:Map/Set/Date 为什么没动静?
  • 规则红线:不可在 build() 修改 state 等硬约束
  • 状态提升 & 局部重绘:用更小的刷新范围换更稳的 FPS
  • 组合拳范式:Store + 装饰器 + 事件
  • 实战小样:一页代码吃透“父传子 + 双向绑定 + 集合变更 + 局部重绘”
  • 收尾与避坑清单

一、装饰器一张表:职责与适用场景(先选对锤子

装饰器 粒度 数据流 何时刷新 典型用法
@State 组件私有 单向(内部自管) 该属性被重新赋值或其可观察包装变化时 组件内部局部状态、UI 临时态
@Prop 子组件 父→子只读 父重新传入新值 子组件显示用,不在子里改它
@Link 父子共享 双向 父或子改都会刷新双方 表单控件、滑块这类“可编辑输入”
@Provide / @Consume 祖先→后代 单向(依赖注入) 提供者更新时广播 主题/全局会话/配置等跨层级共享
@Observed 对象类 属性级 被修饰对象的属性变化触发 可观察数据模型(POJO→响应式)
@ObjectLink 子组件 单个对象链接 链接对象的属性变化触发 大型对象传递且需细颗粒更新
@Watch('stateKey') 方法 —— 被标记的属性变化时回调 派生副作用(轻量 watcher)

记法:读→用 @Prop;写→要么 @State(本地)要么 @Link(父子共治);跨层级→@Provide/ @Consume;复杂对象→@Observed / @ObjectLink

二、刷新边界:到底谁在重建?谁在重绘?

  • 重建(Rebuild)build() 重新执行,子树按依赖重新生成。触发:受影响的可观察属性在本组件内发生变化或父层传参变化。

  • 重绘(Repaint):UI 树已在,只有属性变化导致渲染层更新。

  • 边界规则

    1. @State/@Link 重新赋值会触发本组件重建;
    2. @Observed 对象属性变化可触发依赖它的组件重建,无需整树刷新
    3. @Prop 值变→子组件重建
    4. @ObjectLink 只重建用到该对象属性的子树(更细);
    5. 构造期创建的常量(如 const theme = {})若未纳入可观察体系,改它没用

经验法则:尽量把变化“圈小”。能在子组件内用 @Observed 局部刷新,就别用“父级大 @State”,避免上游牵一发而动全身。

三、父子同步与只读传参:@Prop vs @Link

3.1 只读展示:@Prop

// Parent.ets
@Component
export struct Parent {
  @State title: string = 'Dashboard'
  build() {
    Child({ title: this.title }) // 只读传给子
  }
}

// Child.ets
@Component
export struct Child {
  @Prop title: string // 子里不要改它
  build() {
    Text(this.title).fontSize(20)
  }
}

@Prop 在子里只读,你改它编译器就会提醒你“手别抖”。

3.2 双向绑定:@Link

// Parent.ets
@Component
export struct Parent {
  @State volume: number = 60
  build() {
    VolumeSlider({ value: $volume }) // 注意 $ 协议:传入 link
  }
}

// Child.ets
@Component
export struct VolumeSlider {
  @Link value: number
  build() {
    Slider({ value: this.value })
      .onChange((v:number) => this.value = v) // 改子=改父
  }
}

选型口诀:能只读就不用双向。只有在子组件天然负责输入修改时,再上 @Link,否则状态来源会越来越乱。

四、复杂对象/集合陷阱:Map / Set / Date 为啥没刷新?

4.1 引用类型“原地改”不触发

  • 常见误区:this.list.push(x)有时你会发现没触发重建(取决于框架对该容器的包裹)。
  • 保守与可控的方式:不可变赋值 / 替换引用
@State items: Array<Item> = []

// ✅ 推荐:替换引用,明确触发
this.items = [...this.items, newItem]

// ❌ 风险:原地改可能不被观察到
this.items.push(newItem)

4.2 Map / Set

@State selectedSet: Set<string> = new Set()

// ✅ 推荐:新建集合替换
toggle(id: string) {
  const next = new Set(this.selectedSet)
  next.has(id) ? next.delete(id) : next.add(id)
  this.selectedSet = next
}

4.3 Date

  • date.setHours(10) 修改的是同一对象,可能不触发刷新;
  • 建议this.date = new Date(newValue);或把时间戳作为 number 存在 @State 里。

4.4 可观察对象模型:@Observed / @ObjectLink

@Observed
export class Profile {
  name: string = ''
  age: number = 0
}

// 父
@State user = new Profile()

// 子
@Component
export struct Card {
  @ObjectLink user!: Profile
  build() {
    // 改属性 → 局部重建(而非整棵)
    Button('Birthday +1').onClick(() => this.user.age++)
  }
}

处理“复杂对象”时,优先 @Observed + @ObjectLink,其余集合用替换引用保底。

五、规则红线:这些事儿千万别在 build() 里干

  • build() 修改任何可观察状态:会导致重复构建甚至死循环。

    • ✅ 把初始化放到 aboutToAppear()/onPageShow(),把副作用放到 @Watch 或事件回调里。
  • ❌ 异步回调里直接改已被销毁组件的状态:应在回调前检查存活或在 onDisappear 里取消订阅。

  • ❌ 在子组件里写 @Prop:改变父传入的值。

  • ❌ 把“大对象”塞进 @Link:会带来高频、重成本的同步。

  • ❌ 用“祖先级 @Provide”传一切:层级越深越难追踪,建议主题/会话这类稳定全局才用它。

六、状态提升 & 局部重绘:把刷新圈“画小”

  • 状态提升:当多个子组件需要共享同一份状态时,把状态提升到最近公共父;子靠 @Prop@Link 取用。
  • 局部重绘:把变化频繁的子区块拆成小组件,并给它局部的 @State/ @Observed,避免父级 @State 改动导致整棵树重建。
  • 热点列表:大列表项做成组件,内部使用 @ObjectLink 绑定单行对象,滚动更稳。
// 父只管理“集合引用”,行内细变动由子来控
@Component
export struct ListView {
  @State rows: RowModel[] = [] // 仅增删替换引用
  build() {
    ForEach(this.rows, (row) => ItemRow({ row })) // 子里 @ObjectLink row
  }
}

七、组合拳范式:Store + 装饰器 + 副作用

  • UI 层只管输入与展示(@State/@Prop/@Link)。
  • 数据模型@Observed 做可观察对象;集合采用不可变赋值。
  • 业务副作用@Watch 或“service/store 事件”。
  • 跨层共享优先“显式传参”,其次 @Provide/@Consume(稳定配置/主题)。
@Observed
export class CounterStore {
  value: number = 0
  inc() { this.value++ }
}

@Provide counter = new CounterStore()

@Component
export struct Panel {
  @Consume counter!: CounterStore
  @Watch('counter.value') onValueChanged() { /* 轻副作用 */ }
  build() {
    Row() {
      Text(`${this.counter.value}`)
      Button('+').onClick(() => this.counter.inc())
    }
  }
}

八、实战小样:一页代码吃透“四大要点”

目标:父组件持有集合与主题;子组件 A 只读显示@Prop),子组件 B 可编辑@Link);列表项是 @Observed 对象,子项 细颗粒刷新@ObjectLink);集合变更用不可变赋值所有写操作不在 build()

// models/Todo.ets
@Observed
export class Todo {
  id: string = ''
  title: string = ''
  done: boolean = false
  constructor(id: string, title: string) { this.id = id; this.title = title }
}

// pages/TodoApp.ets
@Component
export struct TodoApp {
  @State themeDark: boolean = false                // 只影响主题的局部刷新
  @State todos: Array<Todo> = [new Todo('1','Learn ArkTS'), new Todo('2','Write Demo')]

  aboutToAppear() { /* fetch → this.todos = [...data.map(x=>new Todo(...))] */ }

  private addTodo(title: string) {
    const id = Math.random().toString(36).slice(2)
    this.todos = [new Todo(id, title), ...this.todos] // 不可变
  }
  private toggleTheme() { this.themeDark = !this.themeDark }

  build() {
    Column({ space: 12 }) {
      Row() {
        Text('ArkTS Todos').fontSize(20)
        Blank()
        Toggle({ type: ToggleType.Switch, isOn: this.themeDark })
          .onChange(() => this.toggleTheme())
      }

      // 只读头部:@Prop
      HeaderBar({ count: this.todos.length })

      // 可编辑输入:@Link
      InputBar({ onAdd: (t:string) => this.addTodo(t) })

      // 列表:行内 @ObjectLink,细颗粒刷新
      ForEach(this.todos, (t) => TodoRow({ item: t }))
    }
    .padding(16).backgroundColor(this.themeDark ? '#111' : '#fafafa')
  }
}

// components/HeaderBar.ets  —— 只读展示
@Component
export struct HeaderBar {
  @Prop count: number
  build() {
    Text(`Total: ${this.count}`).fontSize(16).opacity(0.7)
  }
}

// components/InputBar.ets  —— 父写回调,避免子直接改父集合
@Component
export struct InputBar {
  @Prop onAdd: (t: string) => void
  @State draft: string = ''
  build() {
    Row({ space: 8 }) {
      TextInput({ text: this.draft }).onChange((v:string) => this.draft = v)
      Button('Add').onClick(() => {
        if (this.draft.trim().length) {
          this.onAdd(this.draft.trim())
          this.draft = ''  // ✅ 非 build 场景修改 state
        }
      })
    }
  }
}

// components/TodoRow.ets  —— 行内对象细颗粒刷新
@Component
export struct TodoRow {
  @ObjectLink item!: Todo  // item.done 改变只重建本行

  build() {
    Row({ space: 10 }) {
      Checkbox({ name: 'done', group: 'todo', select: this.item.done })
        .onChange(() => this.item.done = !this.item.done) // ✅ 属性级可观察
      Text(this.item.title)
      Blank()
      Button('Del')
        .onClick(() => EventBus.emit('todo:delete', this.item.id)) // 交给父处理
    }
    .padding(10).backgroundColor('#fff').borderRadius(12)
  }
}

亮点回顾:

  • 掌控集合(不可变替换),掌控对象属性(细颗粒刷新);
  • 只读@Prop输入用回调或 @Link
  • 不在 build() 改状态,所有写入都在事件回调/生命周期里;
  • 列表性能稳:行内对象变化不牵连全列表。

九、收尾与避坑清单(拍胸脯可直接贴到团队 Wiki)

  1. @Prop 就别 @Link;双向绑定请节制,用在“输入控件”即可。
  2. 复杂集合变更 = 替换引用Array/Map/Set 以不可变思路保底。
  3. 对象属性细刷:用 @Observed + @ObjectLink,把刷新圈到单行/单卡片。
  4. 禁止在 build() 写状态:初始化→aboutToAppear,副作用→@Watch 或回调里。
  5. 跨层共享要克制@Provide/@Consume 用在主题/会话等稳定项;业务数据显式传参优先
  6. 拆小组件:热点区域独立成组件,给它自己的 @State 或对象链接。
  7. Date/大对象:用新实例赋值或用原始类型(时间戳),别原地改。
  8. 订阅要解绑:在 onDisappear/onWindowStageDestroy 清理定时器与监听。
  9. 派生状态别存两份:计算得来就get计算,避免“真相有两份”。
  10. 调试刷新:在关键渲染节点打日志,确认是触发了重建(是父传参?还是本地 state?)。

结语

装饰器是好用的,但“用多了就全世界都在刷新”。把数据流理顺把刷新圈画小把复杂对象做到属性级观察,ArkTS 的体验会非常顺滑。下次有人问:“界面不刷怎么办?”——别急着加装饰器,先问一句:**“你真的需要它刷吗?”**😉

如果你愿意,我还能把上面的样例拆成完整可跑工程骨架(含状态管理与测试用例),再补一版性能对照表(不可变 vs 原地改)的压测数据,咱把最佳实践落到底!🚀

(未完待续)

Logo

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

更多推荐