HarmonyOS下饭菜时间 -- @Monitor

一、前言

本系列文章,将带大家深入 API 的底层实现逻辑 ------ 我们会拆解其核心架构与设计思路,探寻官方在开发时重点考量的技术难点、兼容性问题与性能优化方向,还会深挖那些未被覆盖的场景与潜在局限。更重要的是,我们会联动实际开发场景:你在项目中遇到的诡异 Bug、性能瓶颈或使用困惑,是否可能源于 API 的内部设计取舍?希望通过这场深度探索,让大家在源码中找到解决问题的 "隐藏宝藏",沉淀可复用的技术思维。

And 今天团队里有个争论:用 @Monitor 监听多个属性时,如果这些属性同时被修改,会不会导致回调函数被触发多次?

比如这样的代码:

typescript 复制代码
@Monitor('name', 'age', 'email')
onUserInfoChange(m: IMonitor) {
  console.log('用户信息变化了');
  // 处理逻辑...
}

如果我在一个函数里同时修改了 nameageemail,这个 onUserInfoChange 会被调用 3 次吗?

答案是:不会,只会调用一次

我直接去看了源码,发现框架已经做了去重处理。下面说说它是怎么做的,以及整个 @Monitor 的工作机制。

二、@Monitor 的入口和调用链

先说清楚 @Monitor 是怎么被创建和使用的。

入口点

@Monitor 有两个主要入口:

  1. 装饰器方式 :直接用 @Monitor('path1', 'path2') 装饰方法
  2. API 方式 :通过 UIUtils.addMonitor()StateMgmtFactory.makeMonitor() 创建

两种方式最终都会创建 MonitorFunctionDecorator 实例。这个类就是整个监控机制的核心。

调用链

整个流程可以分成几个阶段:

初始化阶段

scss 复制代码
创建 MonitorFunctionDecorator
  → 为每个监控路径创建 MonitorValueInternal
  → readInitialMonitorValues()
    → 设置全局上下文(renderingComponent = RenderingMonitor)
    → 执行 lambda 读取初始值
    → 访问状态对象时触发 addRef(),建立依赖关系

状态变化阶段

scss 复制代码
状态对象被修改
  → MutableStateMeta.fireChange()
    → 遍历 bindingRefs_(所有依赖这个状态的监控器)
    → ObserveSingleton.addDirtyRef()
      → 根据监控器类型:
        - 同步监控器:立即执行
        - 异步监控器:加入队列

批量执行阶段

scss 复制代码
ObserveSingleton.updateDirty()
  → notifyDirtyMonitorPaths() 收集需要执行的监控器
  → 遍历执行 runMonitorFunction()

这个设计的好处是,初始化时就能自动建立依赖关系,不需要手动声明。但代价是依赖全局上下文,代码看起来有点绕。

三、问题背景

先理解一下 @Monitor 的工作机制。当你用 @Monitor 装饰一个方法时,框架会:

  1. 为每个监控路径创建一个 MonitorValueInternal 对象
  2. 这些对象都指向同一个 MonitorFunctionDecorator(也就是你的回调函数)
  3. 当属性变化时,对应的 MonitorValueInternal 会被标记为 dirty

所以理论上,如果 3 个属性同时变化,会有 3 个 MonitorValueInternal 被标记为 dirty。那为什么回调只执行一次?

去重的关键:Set 集合

ObserveSingleton.updateDirty() 这个方法,它是处理所有状态变化的核心:

typescript 复制代码
    public updateDirty(): void {
        do {
            while (this.computedPropRefsChanged_.size > 0) {
                const computedProps = this.computedPropRefsChanged_;
                this.computedPropRefsChanged_ = new Set<WeakRef<ITrackedDecoratorRef>>();
                this.updateDirtyComputedProps(computedProps);
            }
            if (this.persistencePropRefsChanged_.size) {
                const persistenceProps = this.persistencePropRefsChanged_;
                this.persistencePropRefsChanged_ = new Set<WeakRef<ITrackedDecoratorRef>>();
                PersistenceV2Impl.instance().onChangeObserved(persistenceProps);
            }
            if (this.monitorPathRefsChanged_.size > 0) {
                const monitors = this.monitorPathRefsChanged_;
                this.monitorPathRefsChanged_ = new Set<WeakRef<ITrackedDecoratorRef>>();
                let monitorsToRun: Set<MonitorFunctionDecorator> = this.notifyDirtyMonitorPaths(monitors);
                if (monitorsToRun && monitorsToRun.size > 0) {
                    monitorsToRun.forEach((monitor: MonitorFunctionDecorator) => {
                        monitor.runMonitorFunction();
                    });
                }
            }
        } while (
            this.monitorPathRefsChanged_.size +
                this.computedPropRefsChanged_.size +
                this.persistencePropRefsChanged_.size >
            0
        );
    }

注意这里,notifyDirtyMonitorPaths 返回的是 Set<MonitorFunctionDecorator>,而不是 Set<MonitorValueInternal>

这就是关键。看 notifyDirtyMonitorPaths 的实现:

typescript 复制代码
    private notifyDirtyMonitorPaths(monitorPaths: Set<WeakRef<ITrackedDecoratorRef>>): Set<MonitorFunctionDecorator> {
        let monitors: Set<MonitorFunctionDecorator> = new Set<MonitorFunctionDecorator>();
        monitorPaths.forEach((monitorPathRef: WeakRef<ITrackedDecoratorRef>) => {
            let monitorPath = monitorPathRef.deref();
            if (monitorPath) {
                let monitor: MonitorFunctionDecorator = (monitorPath as MonitorValueInternal).monitor;
                if (monitor.isFreeze()) {
                    this.monitorPathRefsDelayed_.add(monitorPathRef);
                } else if (monitor.notifyChangesForPath(monitorPath)) {
                    monitors.add(monitor);
                }
            }
        });
        return monitors;
    }

虽然遍历的是所有变化的 monitorPath(也就是 MonitorValueInternal),但收集到 monitors 集合里的是 monitor(也就是 MonitorFunctionDecorator)。

因为用的是 Set,同一个 MonitorFunctionDecorator 实例只会被添加一次,即使它监控的多个路径都变化了。

四、完整的工作流程

整个流程是这样的:

  1. 属性变化阶段nameageemail 三个属性被修改

    • 每个属性变化都会触发 fireChange()
    • 每个 fireChange() 会把对应的 MonitorValueInternal 加入 monitorPathRefsChanged_ 队列
    • 此时队列里有 3 个 MonitorValueInternal,但它们都指向同一个 MonitorFunctionDecorator
  2. 收集阶段notifyDirtyMonitorPaths 被调用

    • 遍历队列中的 3 个 MonitorValueInternal
    • 提取每个 MonitorValueInternal.monitor(都是同一个 MonitorFunctionDecorator 实例)
    • 因为 Set 的特性,最终 monitors 集合里只有 1 个元素
  3. 执行阶段 :遍历 monitors 集合

    • 只执行一次 runMonitorFunction()
    • 回调函数里可以通过 m.dirty 获取所有变化的路径

这里有个细节:updateDirty() 里有个 do...while 循环,会一直执行直到所有队列都清空。这是因为执行监控器回调时,可能会触发新的状态变化,产生新的依赖更新。这种设计保证了所有变化都能被处理,但也要注意避免无限循环。

一个细节:dirty 数组

虽然回调只执行一次,但你仍然可以知道哪些路径变化了。看 MonitorFunctionDecorator.dirty 的实现:

typescript 复制代码
    public get dirty(): string[] {
        let ret = new Array<string>();
        this.values_.forEach((monitorValue: MonitorValueInternal) => {
            if (monitorValue.dirty) {
                ret.push(monitorValue.path);
            }
        });
        return ret;
    }

它会收集所有标记为 dirty 的路径。所以在回调里,m.dirty 会返回 ['name', 'age', 'email']

同步监控器的特殊情况

上面说的是异步监控器(默认情况)。如果是同步监控器(@SyncMonitorisSynchronous: true),处理方式不同:

typescript 复制代码
    public addDirtyRef(trackedRef: ITrackedDecoratorRef): void {
        if (trackedRef.id >= PersistenceV2Impl.MIN_PERSISTENCE_ID) {
            this.persistencePropRefsChanged_.add(trackedRef.weakThis);
        } else if (trackedRef.id >= MonitorFunctionDecorator.MIN_SYNC_MONITOR_ID) {
            const currentMonitor = (trackedRef as MonitorValueInternal).monitor;
            currentMonitor.notifyChangesForPath(trackedRef);
            currentMonitor.runMonitorFunction();
        } else if (trackedRef.id >= MonitorFunctionDecorator.MIN_MONITOR_ID) {
            this.monitorPathRefsChanged_.add(trackedRef.weakThis);
        } else if (trackedRef.id >= ComputedDec
            oratedVariable.MIN_COMPUTED_ID) {
            this.computedPropRefsChanged_.add(trackedRef.weakThis);
        }
    }

同步监控器在 addDirtyRef 时立即执行,不经过队列。但即使这样,如果同一个监控器的多个路径同时变化,runMonitorFunction 里也有检查:

typescript 复制代码
    public runMonitorFunction(): void {
        if (this.dirty.length === 0) {
            return;
        }
        try {
            this.monitorFunction_(this);
        } catch (e) {
            StateMgmtConsole.log(`Error caught while executing @Monitor function: '${e}'`);
        } finally {
            this.values_.forEach((monitorValue: MonitorValueInternal) => {
                monitorValue.reset();
            });
        }
    }

如果 dirty.length === 0,直接返回,不会执行回调。不过同步监控器在立即执行时,可能还是会有多次调用的风险,取决于属性变化的时机。这个场景比较少见,一般用异步监控器就够了。

五、设计要点和限制

设计要点

  1. 自动依赖追踪 :通过全局上下文(renderingComponent)在读取值时自动建立依赖,不需要手动声明
  2. WeakRef 双向绑定 :用 WeakRef 避免循环引用导致内存泄漏,但需要手动清理失效引用
  3. 批量更新优化:异步监控器批量处理,避免频繁执行
  4. 生命周期管理 :通过 isFreeze() 在组件未激活时冻结监控,避免无效执行

需要注意的限制

虽然框架做了去重,但还有一些限制需要了解:

1. 多个监控器的执行顺序不确定

如果你有多个 @Monitor,它们的执行顺序是不确定的。看代码:

typescript 复制代码
monitorsToRun.forEach((monitor: MonitorFunctionDecorator) => {
    monitor.runMonitorFunction();
});

Set 的遍历顺序是不确定的,所以不要依赖监控器的执行顺序。

2. 不处理循环依赖

如果监控函数里修改了被监控的状态,可能导致无限循环。比如:

typescript 复制代码
@Monitor('count')
onCountChange(m: IMonitor) {
  this.count++; // 危险!可能导致无限循环
}

框架不会检测这种情况,需要开发者自己避免。

3. 不提供事务性

多个状态变化会触发多次回调,不是原子操作。比如:

typescript 复制代码
this.name = 'Alice';
this.age = 20;
this.email = 'alice@example.com';

这三个赋值会触发三次状态变化通知,虽然同一个监控器只会执行一次,但如果有多个监控器,它们可能在不同时机执行。框架不保证这些变化是"原子"的。

4. 不提供防抖/节流

每次变化都会触发,没有内置的防抖或节流机制。如果状态变化很频繁,回调也会频繁执行。需要的话,得在回调函数里自己实现。

5. 同步监控器的特殊行为

同步监控器(@SyncMonitorisSynchronous: true)在状态变化时立即执行,不经过队列。如果同一个监控器的多个路径同时变化,理论上可能执行多次(虽然 runMonitorFunction 里有 dirty.length === 0 的检查,但时机不对的话还是可能多次执行)。

六、总结

所以回答最初的问题:多个属性同时变化时,@Monitor 的回调只会执行一次

去重的机制是:

  • Set<MonitorFunctionDecorator> 收集需要执行的监控器
  • 同一个监控器实例只会被添加一次
  • 即使它监控的多个路径都变化了,最终也只执行一次回调

这个设计是合理的。如果每次属性变化都触发一次回调,性能会有问题,而且逻辑上也不必要------你通常只需要知道"有变化"就够了,具体哪些路径变化了可以通过 m.dirty 获取。

但也要注意框架的限制:执行顺序不确定、不处理循环依赖、不提供事务性、不提供防抖/节流。在实际使用中,要根据这些限制来设计代码,避免踩坑。

七、感谢

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏

各位读者老爷

相关推荐
AlbertZein2 小时前
HarmonyOS一杯冰美式的时间 -- UIUtils基础功能
harmonyos
行者964 小时前
Flutter与OpenHarmony跨平台分享组件深度实践
flutter·harmonyos·鸿蒙
行者964 小时前
Flutter跨平台开发在OpenHarmony上的评分组件实现与优化
开发语言·flutter·harmonyos·鸿蒙
90后的晨仔6 小时前
HarmonyOS 多模块项目中的公共库治理与最佳实践
harmonyos
lili-felicity9 小时前
React Native 鸿蒙跨平台开发:LayoutAnimation 实现鸿蒙端按钮点击的缩放反馈动画
react native·react.js·harmonyos
哈__11 小时前
React Native 鸿蒙跨平台开发:Dimensions 屏幕尺寸获取
react native·华为·harmonyos
奋斗的小青年!!12 小时前
Flutter跨平台开发适配OpenHarmony:手势识别实战应用
flutter·harmonyos·鸿蒙
搬砖的kk13 小时前
Cordova 适配鸿蒙系统(OpenHarmony) 全解析:技术方案、环境搭建与实战开发
华为·开源·harmonyos
不爱吃糖的程序媛13 小时前
OpenHarmony 通用C/C++三方库 标准化鸿蒙化适配
c语言·c++·harmonyos