先看一段很多 V1 项目迁移过来的代码。一个设置页,@ComponentV2 里声明了 @Local theme: string,习惯性地在后面跟了一行 @Watch('onThemeChanged')。编译,没有报错。运行,改主题,onThemeChanged 确实被调用了。你心想“V2 组件里 @Watch 也能用,省事了”,然后继续往下迁。

直到你开始用 PersistenceV2 做持久化。冷启动,onThemeChanged 触发了一次,日志显示恢复出来的值是 'dark',但紧接着 aboutToAppear 里读到的 this.theme 却是 'light'。用户改了一个设置,onThemeChanged 又触发了一次,但这次日志里的时序和上一次完全对不上。你开始怀疑 PersistenceV2 的 connect 写错了,或者 @Watch 在 V2 里“有时候生效有时候不生效”。

问题不在 PersistenceV2,也不在 @Watch 有没有被调用。@Watch 是 V1 的监听器,把它写在 @ComponentV2 的成员上,属于两套状态体系的混用。 官方文档在 V1/V2 混用规则里写得很清楚:@ComponentV2 装饰的自定义组件中不允许使用 V1 装饰器,包括 @Watch;混用可能导致未定义行为,包括冗余重渲染和观测能力丢失。编译期能过的原因,是 @Watch 加在 @Local 这个 V2 状态变量上时,语法层面没有直接冲突;但运行时它面对的是一个 V2 的观测体系,两者的调度模型根本不是同一套。

@Watch 是 V1 的同步监听,V2 的监听器叫 @Monitor

@Watch 的归属是“状态管理(V1)”,它配合的是 @State、@Prop、@Link 这些 V1 变量。它的回调是同步立即执行的:赋值发生的那一刻,回调就跑完了,同一个调用栈里下一行就能读到回调写入的新值。

V2 对应的监听器是 @Monitor,它监听的是 @Local、@Param、@Provider、@Consumer 这些 V2 变量,以及 @ObservedV2 类中 @Trace 修饰的属性。@Monitor 的执行模型是异步合并的:状态变更之后,回调被排入队列,等当前调用栈结束才执行,而且一次用户操作中的多次变更可能被合并成一次回调。

把 @Watch 贴在 @Local 上,你得到的是一个 V1 的同步监听器,去监听一个 V2 的异步观测源。它“看起来在跑”,是因为 V2 的状态变更最终也会触发一些底层通知,@Watch 偶尔能接住;但它的触发时机、触发次数、以及触发时能读到的值,都没有任何契约保证。你写的是一行没有规范背书的代码,它的行为在版本升级、嵌套层级变化、持久化恢复等场景下都会漂移。

PersistenceV2 场景下的“触发两次”,不是 @Watch 坏了,是两套体系各算各的

用 PersistenceV2.globalConnect 恢复一个 @ObservedV2 对象时,从磁盘反序列化出来的数据写回状态对象,对 V2 体系来说是一次真实的状态变更。如果你在这个对象的某个属性上挂了 @Watch,它会被这次“恢复写入”触发一次;用户之后手动修改同一个属性,@Watch 又会被触发一次。

两次触发本身不是错误,它们对应两个不同来源的真实变更。真正的问题是第一次触发时 aboutToAppear 读到的还是默认值。PersistenceV2 的数据是在 connect / globalConnect 被调用时从磁盘反序列化出来并返回对象的,时序由“你在哪里 connect”决定。如果 connect 写在成员初始化里,aboutToAppear 读到的已经是恢复后的对象;如果 connect 放在 aboutToAppear 里,或者在成员初始化时先 new 了一个默认对象、connect 的结果赋给了别处,aboutToAppear 读到的就是那份全新的默认对象,而 @Watch 可能已经在恢复写入时触发过了。两套体系对“什么时候算初始化完成”的理解不一致,你看到的“值对但读不到”就是这种错位的产物。

正确的迁移:@Watch 换成 @Monitor,回调签名和路径都要改

从 V1 迁到 V2,@Watch('onXxx') 不能原样保留。V2 的写法是 @Monitor('path') 装饰一个方法,方法必须接收一个 IMonitor 类型的参数。V1 的回调是无参的,照搬无参方法到 @Monitor 下,编译可能不拦,但运行时回调拿不到变更前后的值,monitor.value() 会返回 undefined,排查方向很容易被带到“监听没触发”上去。

typescript

@ObservedV2
class Settings {
  @Trace theme: string = 'light';
}

@ComponentV2
struct SettingsPage {
  @Local settings: Settings = PersistenceV2.globalConnect({
    type: Settings,
    defaultCreator: () => new Settings()
  })!;

  // 正确:@Monitor + IMonitor 参数,路径指向 @Trace 叶子属性
  @Monitor('settings.theme')
  onThemeChanged(monitor: IMonitor): void {
    const next = monitor.value()?.now;
    console.info(`theme changed to ${next}`);
    // 这里只做副作用,不要在这里依赖级联写入的结果
  }
}

@Monitor 的路径必须逐层可观测:settings 是 @Local 持有的 @ObservedV2 实例,theme 是 @Trace 属性,路径 settings.theme 才成立。如果 Settings 类漏了 @ObservedV2,或者 theme 漏了 @Trace,@Monitor 的回调不会触发,而且不会给出“路径不可观测”的明确报错。

@Monitor 回调里不要写级联逻辑,它不保证同步完成

迁移中最容易踩的第二层坑,是把 V1 里 @Watch 回调中的级联逻辑原封不动搬进 @Monitor。V1 的 @Watch 是同步的,你可以在回调里“改 A → 读 B → 写 C”,同一个调用栈里顺序执行。V2 的 @Monitor 是异步合并的,回调执行时,同一个调用栈里后续的代码可能已经读过了旧值。典型症状是“点两下才更新”。

级联逻辑的正确位置不是 @Monitor 回调。@Monitor 适合做日志、埋点、上报这类不依赖执行顺序的副作用。需要“改一个状态,立刻算出另一个状态”的场景,应该写成一个显式的普通方法,在触发点同步调用,把所有要改的状态一次性算好、写入。

typescript

private switchTheme(theme: string): void {
  const nextLabel = theme === 'dark' ? '深色' : '浅色';
  // 一次性写入全部相关状态,不依赖任何监听器的执行时机
  this.settings.theme = theme;
  this.themeLabel = nextLabel;
}

如果某个存量回调确实无法从“同步执行”的语义中抽离,API 23 及以上提供了 @SyncMonitor,它的行为对齐 V1 的 @Watch,赋值之后同步触发回调,同调用栈内可以读到新值。但它是迁移期的桥,不是新代码的首选。把业务正确性重新绑回监听器的调度策略,只是把问题藏起来了,没有解决“业务逻辑寄生在回调里”的结构问题。

排查顺序:先确认监听器选对没有,再看路径可不可观测

下次 V2 组件里出现“监听有时候生效有时候不生效”或者“恢复后读到默认值”的时候,按这个顺序走。

第一步,全局搜索 @ComponentV2 的 struct 里有没有 @Watch。有,它就是混用。先换成 @Monitor,回调方法补上 IMonitor 参数。@Watch 在 V2 组件里“能跑”是一种没有契约保障的偶然,不是可依赖的行为。

第二步,确认 @Monitor 的路径逐层可观测。路径上的每一层实例是不是 @ObservedV2 且通过 new 创建,叶子属性是不是 @Trace。漏一层,回调就不会触发,而且不会有明确报错。

第三步,如果用了 PersistenceV2,把 connect / globalConnect 放在成员初始化里,而不是 aboutToAppear 内部。aboutToAppear 里读到的应该是恢复后的对象,而不是一份先 new 出来的默认对象。

第四步,检查 @Monitor 回调里有没有“改了 A 立刻读 B”的级联逻辑。有,就抽成显式方法,回调里只留副作用。

@Watch 是 V1 的同步监听,@Monitor 是 V2 的异步合并监听,把它们混在同一个 @ComponentV2 里,编译可能不拦,但运行时没有任何一方为你的时序负责。PersistenceV2 的“触发两次”不是 @Watch 坏了,是恢复写入和用户修改被两套体系各算了一次;aboutToAppear 读到默认值,不是持久化没生效,是 connect 的时序和你读取的位置错开了。下一次 V2 组件里监听行为飘忽的时候,先别怀疑框架——打开那个 struct 看一眼,@Watch 到底还在不在。

Logo

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

更多推荐