“界面卡了就加 `@State`?等等,你确定这是你想要的吗?”——ArkTS 装饰器体系最佳实践全盘点
老实讲,第一次用 ArkTS 做界面,我也干过“哪里不刷新就到处加装饰器”的事儿——结果页面是刷新了,性能也跟着“焕新”了(向下)。这篇我们把 @State / @Prop / @Link / @Provide / @Consume / @Observed / @ObjectLink / @Watch等常用装饰器的边界、传参语义、可观察性陷阱讲清楚,再用一套可运行的微型样例同一数据在父子间同步、只
我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~
前言
老实讲,第一次用 ArkTS 做界面,我也干过“哪里不刷新就到处加装饰器”的事儿——结果页面是刷新了,性能也跟着“焕新”了(向下)。这篇我们把 @State / @Prop / @Link / @Provide / @Consume / @Observed / @ObjectLink / @Watch 等常用装饰器的边界、传参语义、可观察性陷阱讲清楚,再用一套可运行的微型样例收尾:同一数据在父子间同步、只读传参、复杂集合正确刷新、状态提升与局部重绘。不鸡汤,都是踩过的坑🤏。
目录(可当速查清单)
- 装饰器一张表:职责与适用场景
- 刷新边界:谁会触发重建、重绘、谁又不会
- 父子同步与只读传参:
@Propvs@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 树已在,只有属性变化导致渲染层更新。
-
边界规则
@State/@Link重新赋值会触发本组件重建;@Observed对象属性变化可触发依赖它的组件重建,无需整树刷新;@Prop值变→子组件重建;@ObjectLink只重建用到该对象属性的子树(更细);- 构造期创建的常量(如
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)
- 能
@Prop就别@Link;双向绑定请节制,用在“输入控件”即可。 - 复杂集合变更 = 替换引用:
Array/Map/Set以不可变思路保底。 - 对象属性细刷:用
@Observed + @ObjectLink,把刷新圈到单行/单卡片。 - 禁止在
build()写状态:初始化→aboutToAppear,副作用→@Watch或回调里。 - 跨层共享要克制:
@Provide/@Consume用在主题/会话等稳定项;业务数据显式传参优先。 - 拆小组件:热点区域独立成组件,给它自己的
@State或对象链接。 - Date/大对象:用新实例赋值或用原始类型(时间戳),别原地改。
- 订阅要解绑:在
onDisappear/onWindowStageDestroy清理定时器与监听。 - 派生状态别存两份:计算得来就
get计算,避免“真相有两份”。 - 调试刷新:在关键渲染节点打日志,确认是谁触发了重建(是父传参?还是本地 state?)。
结语
装饰器是好用的,但“用多了就全世界都在刷新”。把数据流理顺、把刷新圈画小、把复杂对象做到属性级观察,ArkTS 的体验会非常顺滑。下次有人问:“界面不刷怎么办?”——别急着加装饰器,先问一句:**“你真的需要它刷吗?”**😉
如果你愿意,我还能把上面的样例拆成完整可跑工程骨架(含状态管理与测试用例),再补一版性能对照表(不可变 vs 原地改)的压测数据,咱把最佳实践落到底!🚀
…
(未完待续)
更多推荐




所有评论(0)