一、前言
本系列文章,将带大家深入 API 的底层实现逻辑 ------ 我们会拆解其核心架构与设计思路,探寻官方在开发时重点考量的技术难点、兼容性问题与性能优化方向,还会深挖那些未被覆盖的场景与潜在局限。更重要的是,我们会联动实际开发场景:你在项目中遇到的诡异 Bug、性能瓶颈或使用困惑,是否可能源于 API 的内部设计取舍?希望通过这场深度探索,让大家在源码中找到解决问题的 "隐藏宝藏",沉淀可复用的技术思维。
And 今天团队里有个争论:用 @Monitor 监听多个属性时,如果这些属性同时被修改,会不会导致回调函数被触发多次?
比如这样的代码:
typescript
@Monitor('name', 'age', 'email')
onUserInfoChange(m: IMonitor) {
console.log('用户信息变化了');
// 处理逻辑...
}
如果我在一个函数里同时修改了 name、age 和 email,这个 onUserInfoChange 会被调用 3 次吗?
答案是:不会,只会调用一次。
我直接去看了源码,发现框架已经做了去重处理。下面说说它是怎么做的,以及整个 @Monitor 的工作机制。
二、@Monitor 的入口和调用链
先说清楚 @Monitor 是怎么被创建和使用的。
入口点
@Monitor 有两个主要入口:
- 装饰器方式 :直接用
@Monitor('path1', 'path2')装饰方法 - 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 装饰一个方法时,框架会:
- 为每个监控路径创建一个
MonitorValueInternal对象 - 这些对象都指向同一个
MonitorFunctionDecorator(也就是你的回调函数) - 当属性变化时,对应的
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 实例只会被添加一次,即使它监控的多个路径都变化了。
四、完整的工作流程
整个流程是这样的:
-
属性变化阶段 :
name、age、email三个属性被修改- 每个属性变化都会触发
fireChange() - 每个
fireChange()会把对应的MonitorValueInternal加入monitorPathRefsChanged_队列 - 此时队列里有 3 个
MonitorValueInternal,但它们都指向同一个MonitorFunctionDecorator
- 每个属性变化都会触发
-
收集阶段 :
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']。
同步监控器的特殊情况
上面说的是异步监控器(默认情况)。如果是同步监控器(@SyncMonitor 或 isSynchronous: 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,直接返回,不会执行回调。不过同步监控器在立即执行时,可能还是会有多次调用的风险,取决于属性变化的时机。这个场景比较少见,一般用异步监控器就够了。
五、设计要点和限制
设计要点
- 自动依赖追踪 :通过全局上下文(
renderingComponent)在读取值时自动建立依赖,不需要手动声明 - WeakRef 双向绑定 :用
WeakRef避免循环引用导致内存泄漏,但需要手动清理失效引用 - 批量更新优化:异步监控器批量处理,避免频繁执行
- 生命周期管理 :通过
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. 同步监控器的特殊行为
同步监控器(@SyncMonitor 或 isSynchronous: true)在状态变化时立即执行,不经过队列。如果同一个监控器的多个路径同时变化,理论上可能执行多次(虽然 runMonitorFunction 里有 dirty.length === 0 的检查,但时机不对的话还是可能多次执行)。
六、总结
所以回答最初的问题:多个属性同时变化时,@Monitor 的回调只会执行一次。
去重的机制是:
- 用
Set<MonitorFunctionDecorator>收集需要执行的监控器 - 同一个监控器实例只会被添加一次
- 即使它监控的多个路径都变化了,最终也只执行一次回调
这个设计是合理的。如果每次属性变化都触发一次回调,性能会有问题,而且逻辑上也不必要------你通常只需要知道"有变化"就够了,具体哪些路径变化了可以通过 m.dirty 获取。
但也要注意框架的限制:执行顺序不确定、不处理循环依赖、不提供事务性、不提供防抖/节流。在实际使用中,要根据这些限制来设计代码,避免踩坑。
七、感谢
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏
各位读者老爷