HarmonyOS @Monitor 为什么没触发:@ObservedV2、@Trace 和 @Computed 怎么一起排查状态变化
HarmonyOS @Monitor 为什么没触发:@ObservedV2、@Trace 和 @Computed 怎么一起排查状态变化
有些 ArkUI 状态问题看起来很奇怪:页面里的勾选状态已经变了,底部统计也跟着变了,但日志没有打出来;或者日志正常,统计卡片却重复算了很多次。这个时候只盯着 @State 或 @Local 往往不够,因为问题不一定出在“能不能刷新”,更可能出在“框架到底有没有观察到你改的是哪个字段”。
我会把这个问题拆成两个案例。第一个看单条数据字段变化,重点是 @ObservedV2 和 @Trace。第二个看一批数据替换,重点是 @Computed 不要变成每次渲染都重算的临时函数。

先把几个职责分清楚
官方文档里会把状态管理 V2、MVVM、@Monitor、@Computed 放在一套体系里讲。实际写页面时,我更建议先按职责拆:
| 名称 | 更适合负责什么 | 容易写错的地方 |
|---|---|---|
@ObservedV2 |
声明一个对象可以被 V2 状态系统观察 | 只给外层数组加状态,里面对象还是普通对象 |
@Trace |
标记对象里需要被追踪的字段 | 字段没标,改了也很难被精确监听 |
@Monitor |
监听某条状态路径的变化 | 路径写错,或者监听的是普通对象字段 |
@Computed |
把多个状态算成一个派生值 | 写成普通 getter 后每次刷新都重复跑重逻辑 |
一句话:对象要先能被观察,字段要能被追踪,监听路径才有意义;统计值如果是派生结果,就不要让它散落在多个 build() 分支里重复计算。
案例一:行内字段变了,为什么监听没动
先看一个很常见的写法:列表里每一行是一个任务,点击以后把 done 改成相反状态。
class Task {
id: string = ''
title: string = ''
done: boolean = false
}
@ComponentV2
struct TaskRow {
@Param task: Task = new Task()
@Monitor('task.done')
onDoneChanged(mon: IMonitor) {
console.info(`done changed: ${mon.value()?.before} -> ${mon.value()?.now}`)
}
build() {
Row() {
Checkbox({ name: this.task.id, group: 'task' })
.select(this.task.done)
.onChange((checked: boolean) => {
this.task.done = checked
})
Text(this.task.title)
}
}
}
这段代码看着没问题,实际排查时会有一个风险:Task 只是普通 class,done 也只是普通字段。你改字段时,页面某些地方可能因为外层刷新看起来变了,但 @Monitor('task.done') 不一定能稳定接住这个字段变化。
更稳的写法是把对象和字段都说明白:
@ObservedV2
class Task {
id: string = ''
title: string = ''
@Trace done: boolean = false
constructor(id: string, title: string, done: boolean) {
this.id = id
this.title = title
this.done = done
}
}
@ComponentV2
struct TaskRow {
@Param task: Task = new Task('', '', false)
@Monitor('task.done')
onDoneChanged(mon: IMonitor) {
console.info(`done changed: ${mon.value()?.before} -> ${mon.value()?.now}`)
}
build() {
Row() {
Checkbox({ name: this.task.id, group: 'task' })
.select(this.task.done)
.onChange((checked: boolean) => {
this.task.done = checked
})
Text(this.task.title)
}
}
}
这样排查时就清楚了:Task 是可观察对象,done 是可追踪字段,@Monitor('task.done') 监听的是明确路径。后面如果监听没有触发,优先检查路径、对象来源、是否重新创建了普通对象,而不是怀疑 Checkbox。
案例二:统计卡片为什么重复计算
列表页经常会有“未完成数量”“已选数量”“异常数量”这种统计。如果直接在多个地方写过滤逻辑,页面一刷新就可能到处重算。
@ComponentV2
struct TaskPanel {
@Local tasks: Task[] = []
get unfinishedCount(): number {
return this.tasks.filter((task: Task) => !task.done).length
}
build() {
Column() {
Text(`未完成 ${this.unfinishedCount} 项`)
ForEach(this.tasks, (task: Task) => {
TaskRow({ task })
}, (task: Task) => task.id)
Text(`底部统计:${this.unfinishedCount}`)
}
}
}
这类写法的问题不是一定错,而是规模变大后难排查:上面两个 Text 都会访问 unfinishedCount,如果里面还有排序、分组、权限过滤,重复计算会越来越明显。
用 @Computed 的思路,是把“由状态推导出来的结果”作为一个稳定派生值管理:
@ComponentV2
struct TaskPanel {
@Local tasks: Task[] = []
@Computed
get unfinishedCount(): number {
return this.tasks.filter((task: Task) => !task.done).length
}
build() {
Column() {
Text(`未完成 ${this.unfinishedCount} 项`)
ForEach(this.tasks, (task: Task) => {
TaskRow({ task })
}, (task: Task) => task.id)
Text(`底部统计:${this.unfinishedCount}`)
}
}
}
这里的重点不是少写几行代码,而是把“状态字段变化”和“派生统计变化”分开看。排查时可以先确认 task.done 是否被追踪,再确认 unfinishedCount 是否只在依赖变化时更新。
批量替换数据时要多看一眼
还有一种情况更容易漏:接口返回一批新任务,你直接把数组替换掉。
this.tasks = response.tasks
如果 response.tasks 里每一项都是普通对象,后续行内再改 done,就可能回到案例一的问题。更稳的做法是入口统一转换:
function toTask(raw: TaskDTO): Task {
return new Task(raw.id, raw.title, raw.done)
}
this.tasks = response.tasks.map((item: TaskDTO) => toTask(item))
这个小动作很关键。列表本身换了是一件事,列表项里的字段后续能不能被稳定观察,又是另一件事。不要把服务端 DTO、缓存对象、页面状态对象全混成一个东西。
我本地怎么验证
我写了一个独立脚本模拟两条路径:
- 普通对象字段变化:统计能算出来,但监听日志不会触发;
- 可观察对象字段变化:监听日志能记录前后值,统计值也能跟着变化;
- 批量替换数据:替换入口统一转成
Task,后续字段变化仍然可追踪。
本地运行结果里,坏写法的 monitorTriggered 是 false;稳定写法会记录三条变化日志,最后统计剩 1 个未完成项。这个结果符合预期。
几种写法怎么选
| 写法 | 适合场景 | 不适合场景 |
|---|---|---|
| 普通对象 + 手动刷新 | 临时静态展示 | 列表项字段频繁变化 |
@ObservedV2 + @Trace |
对象字段要被页面和子组件追踪 | 只做一次性 DTO 展示 |
@Monitor |
需要打日志、做联动、排查字段变化 | 代替所有业务逻辑 |
@Computed |
统计、分组、过滤结果依赖状态变化 | 有副作用的网络请求或写库操作 |
我会优先把页面状态对象和 DTO 拆开。DTO 负责接收数据,页面状态对象负责响应 UI。这样改字段、监听字段、计算派生值时,边界会清楚很多。
以后怎么避免
我的检查顺序一般是:
- 看对象是不是
@ObservedV2; - 看被改的字段有没有
@Trace; - 看
@Monitor路径是不是写到了真正变化的字段; - 看数组替换时有没有把普通对象转成页面状态对象;
- 看统计值是不是可以收敛到
@Computed; - 看
@Computed里面有没有写日志、请求、写库这类副作用。
只要按这个顺序排,状态变化问题会少很多。最怕的是页面一处用普通对象,一处用可观察对象,一处又手动重算统计。短期能跑,后面排查起来会非常累。
更多推荐



所有评论(0)