一、前言
大家都是开发,有时候写着写着 ArkUI 代码,突然就会冒出个问号:
"为啥我改个状态变量,UI 就自动刷新了?"
"@Local 到底背后做了啥?能监听到那么精准吗?"
"那 View 更新的时机呢?是 Diff 算法还是真全量更新?"
于是,就顺手点开了 openharmony 的源码,想看看 Local 管理到底是怎么实现的。
一翻不要紧,直接翻到了个叫 StateMgmt
的模块,感觉挺有门道,也挺值得写点什么出来。于是就有了这篇文章(也可能是第一篇,谁知道会不会越写越多呢 )。
因为用V2很久了,所以这次也会用V2的状态管理作为示例。如果想了解V1、V2的差别,V2诞生的背景,请务必点赞收藏分享~
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,如果你想支持下一期请务必点赞~,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏
二、关于状态管理
我们都知道,ArkUI 的核心就是响应式 ------ 改个变量,UI 就能自动更新,不用你手动 setState()
、不用你手动通知。
而实现这一点的底层机制,正式的名字叫做 状态管理(State Management)系统。
什么是状态管理?
状态管理,说人话就是:
"你改的数据,我怎么知道该刷新哪个 UI?"
在 ArkUI 中,开发者习惯于写这样的代码:
scss
@Local count = 0;
build() {
Text(`点击次数:${this.count}`);
}
你只管改 count++
,UI 就自己刷新了。整个过程你不需要管"谁依赖谁""刷新在哪发生的"。这背后,其实就是 状态和 UI 的绑定系统 在默默发力。
而这个"绑定系统"的大本营,就在 OpenHarmony 的这个路径下:
bash
/frameworks/bridge/declarative_frontend/state_mgmt/
在使用V1的时候,大家也应该被@State,@Link等V1的状态装饰器给恶心过,以及各种嵌套数据和UI的使用不便。种种原因都导致了V2的出现。
整个状态管理模块结构大致如下:
ts
state_mgmt/
├── v2/ # 状态管理 V2 的主力实现(你该重点关注的)
├── full_update/ # 全量更新逻辑
└── common/ # 通用工具类、抽象接口
v2/
:所有和现代响应式相关的逻辑都在这,包括 Proxy、装饰器、依赖追踪、UI更新调度等。full_update/
:主打兼容和简单项目用。common/
:工具类、抽象类、日志追踪等通用逻辑。
下面是其中几个核心文件:
文件 | 作用 |
---|---|
v2_view.ts |
UI 节点与状态的绑定逻辑,谁改了要刷谁,就看它 |
v2_observed_proxy.ts |
Proxy 实现,拦截 get/set 实现依赖追踪和变更通知 |
v2_change_observation.ts |
依赖管理器,谁依赖谁、谁变了要通知谁,全靠它调度 |
v2_decorators.ts / v2_decorated_variables.ts |
定义响应式装饰器,比如 @Observed 、@Param 等 |
v2_computed.ts |
实现 @Computed 计算属性 |
v2_monitor.ts |
实现 @Monitor 等装饰器,支持路径监听 |
v2_json_coder.ts / v2_make_observed.ts |
一些数据转换和对象代理辅助方法 |
三、响应式系统的运行机制
"我只是加了个装饰器,UI 怎么就知道我改了值?"(系统猜的0.0)
从你声明变量的那一刻起,它就被盯上了 (实际上是编译完才被盯上)。
看看大致流程:TODO
- 声明变量(@Local)
- 收集依赖(build 时读取变量)
- 通知刷新(变量值变化时触发 UI 更新)
下面我们就按这流程,一步步扒源码。
四、再细一点
装饰器实现
展开说说装饰器吧,先说 @ObservedV2
和 @Trace
是怎么把"一个普通变量"变成"一个响应式变量"的。
核心思路:
- 在组件定义阶段,系统会扫描属性上的装饰器。
- 对于被
@ObservedV2
装饰的属性,会注册元信息。 - 然后重写这个属性,挂上 getter/setter,让它支持依赖追踪和通知更新。
以 @Trace
为例,回顾刚刚的代码吗:
ts
const Trace = (target: Object, propertyKey: string): void => {
// 把这个属性登记为"响应式属性"
ObserveV2.addVariableDecoMeta(target, propertyKey, '@Trace');
// 把原来的属性"改造"成响应式变量
return trackInternal(target, propertyKey);
};
而 trackInternal()
做的事,就是用 Reflect.defineProperty
,把属性换成这样:
ts
Reflect.defineProperty(target, propertyKey, {
get() {
ObserveV2.getObserve().addRef(this, propertyKey); // 依赖收集
return this[`__$${propertyKey}`];
},
set(val) {
this[`__$${propertyKey}`] = val;
ObserveV2.getObserve().fireChange(this, propertyKey); // 通知变更
}
});
这样,一个变量就被包装成了"带钩子的变量"
2.
依赖收集逻辑(ObserveV2
+ Proxy
)
-
OK,现在变量已经"装饰好了",那它是怎么知道谁依赖它、谁要刷新的?
全靠
ObserveV2
。依赖收集怎么做的?
我们知道,组件在 build() 过程中,会读取响应式属性。这个时候 ArkUI 会调用:
tsObserveV2.getObserve().startRecordDependencies(this, elmtId);
然后你在 build() 里写的:
tsText(this.count);
就会触发
count
的 getter,getter 又会调用:tsObserveV2.getObserve().addRef(this, 'count');
从而把
elmtId
和count
之间的依赖关系记录下来。结束之后,再手动调用:tsObserveV2.getObserve().stopRecordDependencies();
整个过程大概可以这么理解:
ts// 开始收集 ObserveV2.startRecordDependencies(this, elmtId); // 在 build 过程中,系统"偷偷记录"你依赖了哪些变量 Text(this.count); // getter → addRef(this, 'count') // 停止收集 ObserveV2.stopRecordDependencies();
最终会得到一个类似这样的结构:
ts// 内部结构大致示意(伪代码) dependencyMap = { 'count' => Set( elmtId1, elmtId2 ) }
状态变更与 UI 刷新
我们现在知道了:
- 谁在用响应式变量(elmtId ← count);
- 变量一变,系统知道该通知谁(反向查找);
接下来就是最后一环------刷新 UI。
fireChange 是谁干的?
当你执行:
ts
this.count++;
实际上走的是 setter,setter 里面会触发:
ts
ObserveV2.getObserve().fireChange(this, 'count');
接下来,就轮到 ViewV2
出场了。
刷新流程:
找到所有依赖了 'count' 的 UI 节点
五、小结
因为篇幅的原因,这篇文章我只大概给大家讲了状态管理 V2 的响应式"主流程":从你写了个 @ObservedV2
开始,到 Proxy 拦截,再到 ObserveV2
记录谁依赖谁,最后通过 ViewV2
把变了的 UI 给刷新了。
这一套流程总结下来就一句话:
你改了谁,系统就只会更新用到"谁"的 UI。
不需要你手动调用刷新、不用你维护订阅关系,也不用你去记住 UI 层更新的细节,整个过程就像"写普通变量"一样丝滑。
当然,很多细节我这篇没展开,比如:
- 装饰器到底是在哪个阶段注册进去的?如何注册进去的?我们能也一样写这样的装饰器吗?
@Computed
那种带缓存逻辑的属性是怎么做到只在依赖变了才重新算?- 依赖关系的数据结构怎么维护?怎么避免内存泄漏?
- 多个属性变了,会批处理更新吗?还是每次都立即刷新?
咕咕咕,如果你感兴趣的话~咕咕咕咕,点个赞吧!
六、总结
[如果有想加入鸿蒙生态的大佬们,快来加入鸿蒙认证吧!初高级证书没获取的,点我!!!!!!!!,我真的很需要求求了,通过立马送美女签名照!]
没了。
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,如果你想支持下一期请务必点赞~,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏