HarmonyOS5 源碼分析 —— 生命周期與狀態(tài)管理(2)
一、前言
在前文中,我们提到过 “状态管理”。
但状态管理并不仅仅是 “数据变化 → UI 更新” 这么简单,它还与组件的 创建、复用、销毁 等生命周期过程密切相关。
理解状态管理与生命周期之间的关系,可以帮助我们规避 内存泄漏、状态残留 等常见问题。
因此,本文将带你一起梳理 生命周期与状态管理的交互机制,并通过示例分析如何在实践中避免踩坑。
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,如果你想支持下一期请务必点赞~,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏
二、阶段一:组件创建与状态初始化
组件刚出生时,状态管理的首要任务就是——初始化状态变量,并为后续的依赖收集准备好“档案”。
-
构造函数中的状态初始化
在构造函数执行阶段,状态管理相关的数据结构会被搭好,但此时 依赖收集还没开工。可以理解为,你先把仓库搭起来,后面才能往里面存货。
(看完图你就可以跳到八了,嘻嘻)
-
装饰器元信息的注册
装饰器在构造函数执行时就会注册元信息,为后续的依赖收集做准备。
// ViewV2 构造函数 constructor(parent: IView, elmtId: number = UINodeRegisterProxy.notRecordingDependencies, extraInfo: ExtraInfo = undefined) { super(parent, elmtId, extraInfo); // 状态变量在这里初始化 this.dirtDescendantElementIds_ = new Set<number>(); this.monitorIdsDelayedUpdate = new Set(); this.computedIdsDelayedUpdate = new Set(); // ... 其他状态初始化 }
三、阶段2:依赖收集与UI渲染
当组件进入渲染阶段时,状态管理系统会开始记录 哪些 UI 元素依赖了哪些状态变量。
-
开始依赖收集
收集必须发生在
startRecordDependencies
和stopRecordDependencies
之间,这样依赖关系才不会乱。// @ObservedV2 装饰器 function ObservedV2<T extends ConstructorV2>(BaseClass: T): T { ConfigureStateMgmt.instance.usingV2ObservedTrack(`@ObservedV2`, BaseClass?.name); return observedV2Internal<T>(BaseClass); } // observedV2Internal 内部实现 const observedClass = class extends BaseClass { constructor(...args) { super(...args); // 在构造函数中注册装饰器元信息 // 这些元信息后续用于依赖收集和变更通知 } }
-
状态变量的读取与依赖建立
每次读取状态变量时,系统会记下:“当前渲染的
elmtId
依赖了这个属性”。这就像贴便利贴,方便后续精准更新。// ViewV2.observeComponentCreation2 public observeComponentCreation2(compilerAssignedUpdateFunc: UpdateFunc, classObject: { prototype: Object, pop?: () => void }): void { // ... 前置处理 ... const updateFunc = (elmtId: number, isFirstRender: boolean): void => { // 关键:开始依赖收集 ObserveV2.getObserve().startRecordDependencies(this, elmtId); try { // 执行 build 函数,渲染UI compilerAssignedUpdateFunc(elmtId, isFirstRender); } finally { // 关键:停止依赖收集 ObserveV2.getObserve().stopRecordDependencies(); } }; }
四、阶段3:状态变更与UI更新
这里有个误区:状态变更不是立刻就让 UI 刷新。
HarmonyOS 走的是“先拦截、再批处理”的策略,减少性能损耗。
-
状态变更的拦截
系统用
Proxy
或getter/setter
来捕捉状态变化,然后发出“更新通知”。// @Trace 装饰器的 getter const Trace = (target: Object, propertyKey: string): void => { ObserveV2.addVariableDecoMeta(target, propertyKey, '@Trace'); return trackInternal(target, propertyKey); }; // trackInternal 内部实现(伪代码) function trackInternal(target: Object, propertyKey: string) { Reflect.defineProperty(target, propertyKey, { get() { // 关键:在 getter 中收集依赖 ObserveV2.getObserve().addRef(this, propertyKey); return this[ObserveV2.OB_PREFIX + propertyKey]; }, set(val) { // 关键:在 setter 中通知变更 if (val !== this[ObserveV2.OB_PREFIX + propertyKey]) { this[ObserveV2.OB_PREFIX + propertyKey] = val; ObserveV2.getObserve().fireChange(this, propertyKey); } } }); }
-
变更通知与UI标记
不是马上更新,而是先把需要刷新的节点ID丢进“脏集合”,等待统一处理。
// ObjectProxyHandler 的 set 方法 set(target: any, key: string | symbol, value: any): boolean { const oldValue = target[key]; if (oldValue === value) { return true; // 值没变,不触发更新 } // 设置新值 target[key] = value; // 关键:通知变更 ObserveV2.getObserve().fireChange(conditionalTarget, key); return true; }
-
批量UI更新
等批量处理时,一次性刷新相关 UI,避免“频繁改一行代码、刷新整个页面”的低效操作。
// ViewV2.uiNodeNeedUpdateV2 public uiNodeNeedUpdateV2(elmtId: number): void { // 关键:将需要更新的 elmtId 加入脏集合 this.dirtDescendantElementIds_.add(elmtId); // 创建属性变更函数 const propertyChangedFunc: PrebuildFunc = () => { // 这里可以执行一些前置逻辑 }; // 标记需要更新 this.markDirtyElement(elmtId, propertyChangedFunc); }
五、阶段4:组件复用与状态重置
组件不是用完就丢,HarmonyOS 会用 RecyclePoolV2
来“回收再利用”。
-
组件复用的触发
复用时会调用
aboutToReuseInternal
把状态重置干净,像清空缓存一样。// ViewV2.updateDirtyElements public updateDirtyElements(): void { // 获取所有需要更新的 elmtId const dirtElmtIdsFromRootNode = Array.from(this.dirtDescendantElementIds_); // 批量更新 dirtElmtIdsFromRootNode.forEach(elmtId => { this.UpdateElement(elmtId); // 更新完成后从脏集合中移除 this.dirtDescendantElementIds_.delete(elmtId); }); }
-
状态变量的重置
保证下次用这个组件时,它的状态是全新的,不会“带病上岗”。
// ViewV2.reuseOrCreateNewComponent public reuseOrCreateNewComponent(params: { componentClass: any, getParams: () => Object, getReuseId?: () => string, extraInfo?: ExtraInfo }): void { const reuseId = params.getReuseId?.() || 'default'; const recyclePool = this.getOrCreateRecyclePool(); // 尝试从回收池中获取可复用的组件 let reuseComp = recyclePool.popRecycleV2Component(reuseId); if (reuseComp) { // 复用现有组件 reuseComp.aboutToReuseInternal(params.getParams()); // ... 其他复用逻辑 } else { // 创建新组件 // ... } }
-
依赖关系的重建
不光是数据要重置,监听器、计算属性、消费者这些依赖也要全部更新,避免数据错乱。
// ViewV2.resetStateVarsOnReuse public resetStateVarsOnReuse(params: Object): void { // 重置所有状态变量 Object.keys(params).forEach(key => { this.resetParam(key, params[key]); }); } // VariableUtilV2.resetParam public static resetParam<Z>(target: object, attrName: string, newValue: Z): void { const meta = target[ObserveV2.V2_DECO_META]?.[attrName]; VariableUtilV2.checkInvalidUsage(meta, attrName); const storeProp = ObserveV2.OB_PREFIX + attrName; if (newValue === target[storeProp]) { return; // 值没变,不触发更新 } // 设置新值并通知变更 target[storeProp] = newValue; ObserveV2.getObserve().fireChange(target, attrName); }
六、 阶段5:组件销毁与清理
最后一步,就是断干净一切联系,防止内存泄漏。
-
依赖关系的清理
销毁时要清空状态变量、监听器、计算属性和消费者,让组件彻底“归零”。
// ViewV2.aboutToReuseInternal private aboutToReuseInternal(initialParams?: Object): void { // 重置状态变量 if (initialParams) { this.resetStateVarsOnReuse(initialParams); } // 重置监听器和计算属性 this.resetMonitorsOnReuse(); Object.keys(this.computedIdsDelayedUpdate).forEach(name => { this.resetComputed(name); }); // 重置消费者 this.resetConsumer('', undefined); }
七、值得我们小心的
-
避免在 build 中进行复杂计算
相信大家可能会写出这样的代码:
build() { Colum(){ Text(this.heavyComputation().toString()) } }
显示一切正常
但是build 方法可能被频繁调用,复杂计算会严重影响性能。而@Computed 提供了智能缓存机制。所以更推荐你这么写:
@Computed get expensiveResult() { return this.heavyComputation(); // 只在依赖变化时执行 } build() { Colum(){ Text(this.expensiveResult.toString()) } }
-
合理使用 @Monitor 避免过度监听
Monitor每一个用过的人都说爽,爽过头你就会写出这样的代码:
@Monitor('user', 'profile', 'settings', 'preferences', 'theme') onUserChange() { // 任何相关变化都会触发 }
过度监听会导致不必要的回调执行,影响性能。应该只监听真正关心的数据路径。
//只监听必要的路径 @Monitor('user.profile') onProfileChange() { // 只在 profile 变化时触发 }
-
组件复用时的状态重置
在repeat组件中,有时候会出现一些奇怪的显示结果,或者显示结果和事件结果不对应,可能就是因为组件被复用了:
//依赖持久状态 @Trace lastUpdateTime = Date.now(); aboutToReuse() { // 这个时间戳在组件复用时不会重置! console.log('上次更新:', this.lastUpdateTime); }
组件复用时,装饰器管理的状态会自动重置,但普通属性不会。这可能导致状态不一致。
//在 aboutToReuse 中重置状态 aboutToReuse() { this.lastUpdateTime = Date.now(); // 手动重置 }
如上!Very GGGood!
-
异步操作中的依赖收集
跨线程执行(比如 ArkUI
TaskPool
)时,代码运行在 Worker 线程,是没有直接访问 UI 响应式对象的能力的。即使访问到了,也只是访问了数据的“副本”,而不是原始的响应式代理对象。
这样
this.count++
只会改副本,不会触发主线程的 UI 更新。// ❌ 错误:在异步回调中直接修改状态 setTimeout(() => { this.count++; // 可能没有正确的依赖上下文 }, 1000);
异步操作可能发生在依赖收集上下文之外,直接修改状态可能导致依赖关系丢失。
// ✅ 正确:使用状态管理的方法 setTimeout(() => { this.updateCount(this.count + 1); // 通过方法更新 }, 1000); private updateCount(newValue: number) { this.count = newValue; // 在正确的方法上下文中 }
-
状态变量的命名规范、
开发中,我要使用清晰的,意义明确的命名
// 清晰的命名 @Trace userName = ''; @Trace userAge = 0; @Trace isUserLoggedIn = false; //模糊的命名 @Trace data = {}; // 不清楚具体是什么数据 @Trace flag = true; // 不清楚标志的含义
你也不想搜个flag,一看200个的情况出现吧。
八、小结
大概就这些啦,总结下:
从组件的 创建与初始化 到 依赖收集、状态变更,再到 复用与重置,直至 销毁与清理,生命周期的每一个阶段都与状态管理息息相关。
只有真正理解它们之间的交互机制,我们才能写出 高性能、无隐患、易维护 的应用。掌握了这些底层逻辑,你会发现——很多“莫名其妙”的 UI Bug,其实都有迹可循;而那些难以捉摸的内存泄漏,也能被提前规避。
(那你知道为什么V2的属性全是__ob_开头了吗?)
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,如果你想支持下一期请务必点赞~,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏
八、大结
看这 -------------------->[如果有想加入鸿蒙生态的大佬们,快来加入鸿蒙认证吧!初高级证书没获取的,点我!!!!!!!!,我真的很需要求求了,通过立马送美女签名照!]<--------------------看这
没了。
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,如果你想支持下一期请务必点赞~,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏
共同學(xué)習(xí),寫下你的評(píng)論
評(píng)論加載中...
作者其他優(yōu)質(zhì)文章