@Trace 加了 UI 还是不动,ArkUI V2 的观测链断在哪
接口返回 JSON,你 JSON.parse 之后直接赋给 @Local 修饰的数组,页面第一次能渲染出来——因为初始赋值本身会触发一次刷新。然后用户在输入框里改了某个字段的值,console.log 打印出来确实变了,但屏幕上那个 Text 纹丝不动。你回头检查,@ObservedV2 加了,@Trace 也加了,对象确实是在状态的链路里。问题不是“没加装饰器”,而是你以为加完装饰器就自动拥有了深度观测——V2 的观测能力有前提,前提不满足,装饰器写了也等于没写。
观测能力从 new 开始,不从 JSON.parse 开始
V1 的 @State 修饰对象时,能自动感知第一层属性的变化,嵌套再深就力不从心了。V2 把这件事做得更细:@ObservedV2 和 @Trace 配合,理论上能做到属性级的精确刷新。但代价是,它要求你交给状态树的对象本身具备被观测的能力。
JSON.parse 产出的是一棵普通的对象树,没有任何装饰器生成的代理层。你把它塞进 @Local orders: Order[],ArkUI 不会替你自动“升级”它。文档的原话是,使用 @ObservedV2 与 @Trace 装饰的类,需要通过 new 实例化之后,才具备被观测变化的能力。JSON.parse、对象字面量直接赋值,产出的都是普通实例,修改它们的属性不会触发依赖通知。
所以正确做法不是“解析完直接用”,而是解析完之后做一次映射,把普通数据灌进可观测类的构造函数。
typescript
@ObservedV2
class Order {
@Trace orderId: string = '';
@Trace amount: number = 0;
constructor(orderId: string, amount: number) {
this.orderId = orderId;
this.amount = amount;
}
}
// 错误:JSON.parse 的产物没有观测能力
// this.orders = JSON.parse(jsonStr) as Order[];
// 正确:用 new 构造,观测链从根上建立
const raw = JSON.parse(jsonStr) as Array<{ orderId: string; amount: number }>;
this.orders = raw.map(item => new Order(item.orderId, item.amount));
map 那一行是关键。它不只是“换个写法”,而是把每个元素从普通对象替换成了带有 @ObservedV2 元信息的实例。没有这一步,后面无论你怎么改 order.amount,UI 都不会重新求值。
三层以上的嵌套同样如此。只给最外层加 @ObservedV2 不够,链路上每一层参与刷新的类都要加,每个需要触发刷新的属性都要加 @Trace。任何一层是普通类,观测链就在那一层断掉,断点以下的变化全部静默。
typescript
@ObservedV2
class Address {
@Trace city: string = '';
constructor(city: string) { this.city = city; }
}
@ObservedV2
class Order {
@Trace address: Address;
constructor(address: Address) { this.address = address; }
}
// Address 和 Order 都用了 new,链路完整
const order = new Order(new Address('北京'));
order.address.city = '深圳'; // 触发刷新
如果 Address 忘了加 @ObservedV2,或者 address 字段忘了加 @Trace,order.address.city = '深圳' 这一行在日志里能看到值变了,但界面不会有任何反应。不是 V2 的 bug,是依赖链在中间断了。
@Monitor 监听不到,往往是因为路径不够精确
@Monitor 的深度监听有一个容易被忽略的前提:监听路径上的每一层都要是可观测的。路径上任何一层漏了 @Trace,@Monitor 的回调就不会触发。
还有一个更隐蔽的情况:当 @Monitor 监听的是整个数组时,修改数组中某一项的属性,不一定能触发回调。比如 @Monitor('orders') 监听 orders 这个数组变量,orders[0].amount = 100 改的是元素内部属性,不是数组引用本身。要精确感知到这种变化,需要把监听路径写到元素级别,或者利用新版本对通配符的支持。文档里明确提到,监听整个数组时,修改数组某一项的属性需要监听具体路径,而不是只监听数组名。
排查时可以做一个很直接的验证:把 @Monitor 的路径从数组名改成 'orders[0].amount',如果回调触发了,说明之前的路径粒度太粗,问题不在观测链,在监听声明。
整对象替换会让局部刷新退化成全局重建
V2 的精确刷新依赖一个前提:你修改的是 @Trace 属性的值,而不是替换持有该属性的对象引用。直接给字段赋值,比如 order.amount = 200,只会通知 build 中实际读取了 amount 的那部分组件。但如果你写 order = { ...order, amount: 200 },引用变了,所有通过 @Param 接收 order 的子组件都会因为“父组件传下来的对象变了”而刷新,哪怕它们只读了 orderId。
这就是“只改了一个字段,列表却整片卡顿”的常见来源。对象字面量展开、深拷贝、重新 new 一个同值对象,效果都一样:引用变,刷新边界就从属性级退回到组件级。V2 给你属性级精确刷新的能力,但用整对象替换去触发更新,等于主动放弃了这个能力。
要改字段就直接写 order.amount = 200,不要绕回“构造一个新对象再赋回去”的老路。V2 的 @Trace 属性本身就具备被赋值并触发通知的能力,不需要靠替换引用来“告诉框架数据变了”。
V1 和 V2 不要在同一条链路上各用各的
@State、@Observed、@ObjectLink 属于 V1,@Local、@Param、@ObservedV2、@Trace 属于 V2。两者在同一对象链路上混用,行为会变得不可预期:@State 只观测第一层,它如果持有一个 @ObservedV2 的嵌套对象,嵌套属性的变化它感知不到;反过来,V2 的类也不应该被 V1 的 @Observed 去装饰。
排查的时候直接看同一条数据通路上有没有两套装饰器并存。如果有,统一到一套。V2 项目里就用 @Local 声明根状态,用 @Param 接收子组件参数,嵌套类统一 @ObservedV2 + @Trace,不要在中间某个组件里突然冒出一个 @State 去接 V2 的对象。
设计状态边界,比事后排查更省力
观测链断不断,很多时候在定义数据模型的时候就决定了。一个大对象从页面根节点一路 @Param 传到叶子组件,中间任何一层漏标,或者任何一个子组件只用到其中一个字段却因为整对象替换被带着刷新,都是结构问题,不是某个装饰器写错的问题。
比较稳的做法是按业务域拆状态类,让每个区域只依赖自己那块数据;把读取同一批 @Trace 属性的 UI 片段拆成独立的 @ComponentV2 子组件,让刷新边界和组件边界对齐;子组件要改数据就通过 @Event 回调回源头,不要在子组件里复制一份再改副本。V2 的精确刷新不是“加了 @Trace 就自动发生”的,它需要你的数据结构和组件结构配合:属性赋值、逐层可观测、不换引用,三件事同时成立,刷新才会落在最小的那个点上。
更多推荐

所有评论(0)