ArkTS 状态管理 V1 迁 V2 实录:@Observed 不生效的 5 种情况,和一份 API 12 迁移清单

🔑 关键词:鸿蒙开发,ArkTS,状态管理V2,HarmonyOS NEXT,@ObservedV2

📖 摘要:一个真实项目里的 ArkTS 状态管理迁移记录:V1 为什么在嵌套对象上必然失效、@Track 到底补了哪个洞、V2 的 @Trace 把观测责任从视图层挪到数据层意味着什么,以及 12 个页面、2.3 万行代码的迁移顺序和踩坑清单。

一、界面不动的那个下午

图片

去年 11 月我接手一个订单类的鸿蒙应用,compatibleSdkVersion 写着 5.0.0(12),SDK 是新的,装饰器全是旧的——@State、@Prop、@ObjectLink 那一套。前面几个扁平页面跑得挺好,直到我把「收货地址」从订单对象里拆出来,做成一个二级对象塞进 @State。改数据、打日志、console.log 打出来都对,界面纹丝不动。

我那一小时的排查顺序大概是:先怀疑是没刷新 → 加 @Observed → 没用 → 加 @ObjectLink → 部分好了 → 再嵌一层又坏 → 去翻文档 → 发现 API 12 早就把状态管理重做了一遍。这段经历后来我跟三个做鸿蒙的同行聊过,三个人都踩过同一个坑。这不是巧合,也不是谁写错了。

二、V1 的观测边界,比文档写得还窄

先说结论:V1 的观测粒度是一层,而且这一层是「属性级」而不是「对象级」。

图片

@State 能观测到的是两种情况:整个变量被重新赋值(this.obj = newObj),以及它第一层属性的赋值(this.obj.name = 'x')。再往下就不行了——this.obj.address.city = 'x',框架根本不知道你动了东西。

@Observed + @ObjectLink 这套组合是用来补这个洞的,但补得很别扭:你必须在用到那个嵌套对象的那一级组件上加 @ObjectLink。注意这句话的主语——是视图组件,不是数据类。数据是不是可观测,由写 UI 的人决定,而写数据类的人压根不知道这件事。我见过的代码里,@ObjectLink 被加在离数据定义隔了三层的地方,换了个人接手、用另一个页面引用同一个类,一样不刷新。这不是谁写错了,这是职责放错了地方。

三、@Track 是补丁,不是解法

API 11 给了个 @Track。用法是在 @Observed 修饰的类里给部分属性加 @Track 标记,框架只观测被标记的属性,其他属性变化不触发刷新。对大对象来说这是好事,能省掉一堆无意义的刷新。

图片

但它没解决嵌套。被 @Track 标记的属性如果本身是个对象,对象内部的字段变化照样观测不到。@Track 优化的是「刷新范围」,不是「观测深度」。这两个问题经常被混为一谈,我翻文档那会儿也没一下看明白,直到在一个 200 项的列表里做实验才确认。

四、V2 不是升级,是把问题重新定义了一遍

@ObservedV2 + @Trace 这一套(API 12 随 5.0.0(12) 一起来)做的最大改变,是把观测点从视图层挪到了数据层。你现在长这样:

@ObservedV2
class Address {
  @Trace city: string = '';
  @Trace detail: string = '';
}



![图片](http://img2.baidu.com/it/u=2128744886,1463650613&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=1044)


@ObservedV2
class Order {
  @Trace id: string = '';
  @Trace address: Address = new Address();
}

只要在类定义的地方标了 @Trace,嵌套多深都能观测到,视图那边不需要再写任何多余的东西。这不是 API 的变化,是职责边界的变化——数据是否可观测,由定义数据的人决定。

我个人的判断是:ArkTS 把 TS 的动态性砍掉之后(不允许 any、结构化类型不生效、运行时代理能力受限),它就没法做出一套靠运行时深度拦截的响应式系统。V1 是在这个约束下硬凑出来的,V2 是承认约束之后的重设计——用编译期的装饰器去补运行时缺失的拦截能力。这是我从行为上倒推的理解,没去翻方舟运行时源码,但只有这个解释说得通。

图片

五、一份装饰器对照表

V1 V2 关键差异
@State @Local 只能在组件内部初始化,不再接受父组件直接传值
@Prop @Param 单向只读,子组件改不了;要本地改配 @Once
@Link @Param + @Event 双向绑定被拆成「单向数据 + 事件回调」
@Provide / @Consume @Provider / @Consumer 功能类似,@Consumer 支持设默认值
@Watch @Monitor 可一次监听多个属性,回调直接给 oldValue / newValue
@Observed / @ObjectLink @ObservedV2 / @Trace 观测点从视图层移到数据层
@Track @Trace @Trace 支持嵌套类,@Track 只解决刷新范围

六、迁移实操:我按这个顺序做的

  1. 先动数据类,不动 UI。 把所有 @Observed 的类改成 @ObservedV2,给需要的字段加 @Trace。这一步可以整体编译通过,V1 组件照样能读 @ObservedV2 的数据,因为 @Trace 是数据层能力,V1 的状态变量能观测到它的变化。
  2. 按组件逐个迁。 一个 .ets 文件里不能同时出现 @Component 和 @ComponentV2,所以必须整组件地换,不能一行一行改。我的做法是从叶子组件往上换,先换没有状态依赖的纯展示组件,风险最小。
  3. @Link 全部改成 @Param + @Event。 这一步最费时间。双向绑定拆成单向之后,原本「子组件改、父组件自动同步」的逻辑要显式写回调。我大概花了两天才把感觉找回来。
  4. @Watch 换 @Monitor。 注意 @Monitor 回调里不要改被监听的属性,会循环触发。我第一版就写出了死循环,日志刷屏刷到应用卡住,排查了四十分钟。
  5. @Provide/@Consume 换 @Provider/@Consumer。 这一对改动最小,但要注意 @Consumer 如果找不到上层 @Provider,会走默认值而不是报错,容易埋雷。

图片

整个过程在 12 个页面、约 2.3 万行的工程上花了六天,其中三天全耗在第 3 步。

七、两个不值得迁的信号

一是你的数据模型就是扁平的,没有三层以上嵌套,V1 完全够用,换 V2 只是给自己找活干。二是工程里大量引用了第三方 HAR,那些包内部还是 V1,你迁完之后混着跑,调试起来会很烦。

反过来,如果你的数据模型本身就是树形的(订单-商品-规格、组织-部门-人员),V2 的收益会立刻显出来。我在迁移之后做过一次对比:一个 200 项、每项带两层嵌套的列表,同一份数据下 V1 加 @Track 后单次编辑触发约 40 次组件刷新,V2 是 3 次。这个差距不是靠调优能补上的,它来自观测点位置的不同。

🏷️ 标签: