ArkUI Engine - 探索状态管理装饰器的实现

ArkUI Engine - 探索状态管理装饰器的实现

技术博客 admin 508 浏览

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

ArkUI 状态管理

文章介绍

从前几篇文章中,我们了解到ArkUI中针对UI进行刷新处理流程,对于应用开发者来说,ArkUI把驱动UI刷新的一系列操作通过状态管理装饰器,比如@State,@Prop等暴露给开发者,通过引用这些装饰器修改的变量,我们能够实现自动的UI刷新处理。

值得注意的是,本章会涉及到api 9以上的内容,比如api 11,这些内容虽然在华为官网还没有暴露给普通开发者,但是我们可以通过open harmony docs 中提取查看这些内容,比如@Track 装饰器。 因此如果大家想要提前了解鸿蒙Next的api,即便公司没有和华为签约,我们也还是能够通过open harmony docs去查看更高版本的内容。

本篇我们讲进入状态管理相关内容的学习,通过针对状态管理的学习,我们能够了解到以下知识点:

  1. 了解常见的状态管理,比如@State装饰器 是如何进行状态刷新
  2. 鸿蒙api 9 class全量刷新导致的问题以及ArkUI后续优化的实现原理,比如@Track 装饰器
  3. 了解到属性刷新驱动UI刷新的过程

状态管理例子

我们拿一个最简单的例子介绍一下状态管理,我们定义一个StateCase的Component,其中有一个button,当点击button的时候就会改变当前的ui展示,这里面的ui数据通过showRow这边变量管理

scss
复制代码
@Component struct StateCase{ @State showRow:TestTrack = new TestTrack() build(){ Row(){ if (this.showRow.param1){ Row(){ Text("i am row") } }else{ Column(){ Text("i am colomn") } } Button("点我").onClick(()=>{ this.showRow.param1 = !this.showRow.param1 }) } } } class TestTrack{ param1:boolean = true }

@State 修饰的变量,最终会被编译为一个ObservedPropertyObjectPU类的实现

kotlin
复制代码
class StateCase extends ViewPU { constructor(parent, params, __localStorage, elmtId = -1) { super(parent, __localStorage, elmtId); this.__showRow = new ObservedPropertyObjectPU(new TestTrack(), this, "showRow"); this.setInitiallyProvidedValue(params); } setInitiallyProvidedValue(params) { if (params.showRow !== undefined) { this.showRow = params.showRow; } } __showRow 的get方法 get () { return this.__showRow.get(); } __showRow的set方法 set showRow(newValue) { this.__showRow.set(newValue); }

同时针对showRow的访问,都会被替换为针对__showRow变量的访问,比如下面的set与get方法。比如当我们点击button的时候,实际上就是调用了this.__showRow.set(newValue); 进行新值的赋予。

javascript
复制代码
this.observeComponentCreation((elmtId, isInitialRender) => { .... Button.onClick(() => { this.showRow.param1 = !this.showRow.param1; }); .... });

这里我们就要停下来思考一下,ArkTS是怎么在TS的基础上实现的响应式?

响应式,其实本质上都是通过回调的思想实现的,ArkTS中在内部把这些回调的细节统统隐藏了,因此开发者们可以在不用关心这些细节的基础上,就很容易的实现UI的刷新。下面我们就来看,为什么ArkTS要千辛万苦的把我们普通声明的状态变量变成一个ObservedPropertyObjectPU对象。

ObservedPropertyObjectPU 如何驱动UI刷新

ObservedPropertyObjectPU 的实现在ArkUI engine的/state_mgmt/src/lib/common 中,其实它是继承了ObservedPropertyPU的一个实现,ObservedPropertyObjectPU里面其实所有的set get方法,都会调用ObservedPropertyPU的set/get方法。对应着上文例子中的__showRow get/set方法

scala
复制代码
// class definitions for backward compatibility class ObservedPropertyObjectPU<T> extends ObservedPropertyPU<T> { }

我们来简单看一下ObservedPropertyPU的内部实现

ObservedPropertyPU set方法

当外部UI需要发生改变的时候,就会通过set方法进行复制,比如改变 this.showRow.param1

kotlin
复制代码
Button("点我").onClick(()=>{ this.showRow.param1 = !this.showRow.param1 })

实际上调用的就是set方法this.showRow的set方法,我们拿arkui 4.1分支代码查看。这里注意,engine 4.1其实就是api11的代码,大家能够在openharmony中查看最新的代码分支情况,这些都是未来鸿蒙next的代码。即使个人开发者现在只能用api9的内容,但是我们我们还是可以查看到未公开的api11代码细节。【api 11代码能够更方便我们了解api9的一些状态管理弊端以及后续的优化方向】

kotlin
复制代码
ObservedPropertyPU 类中 set方法 public set(newValue: T): void { 如果两者是同一个变量,用=== 判断,则直接return不进行刷新,本次是无效刷新 if (this.wrappedValue_ === newValue) { stateMgmtConsole.debug(`ObservedPropertyObjectPU[${this.id__()}, '${this.info() || "unknown"}']: set with unchanged value - ignoring.`); return; } stateMgmtConsole.propertyAccess(`${this.debugInfo()}: set: value about to changed.`); 把旧的,也就是上一个值用oldValue变量记录,方便后续进行UI刷新的判断。 const oldValue = this.wrappedValue_; setValueInternal方法中会把this.wrappedValue_ 更新为newValue if (this.setValueInternal(newValue)) { TrackedObject.notifyObjectValueAssignment(/* old value */ oldValue, /* new value */ this.wrappedValue_, 这里触发了UI刷新,在鸿蒙api 9 的版本只会走notifyPropertyHasChangedPU里面的内容刷新,这里大家可以思考一下 this.notifyPropertyHasChangedPU.bind(this), this.notifyTrackedObjectPropertyHasChanged.bind(this)); } } 状态复制管理 private setValueInternal(newValue: T): boolean { stateMgmtProfiler.begin("ObservedPropertyPU.setValueInternal"); if (newValue === this.wrappedValue_) { stateMgmtConsole.debug(`ObservedPropertyObjectPU[${this.id__()}, '${this.info() || "unknown"}'] newValue unchanged`); stateMgmtProfiler.end(); return false; } if (!this.checkIsSupportedValue(newValue)) { stateMgmtProfiler.end(); return false; } // 解除旧的绑定 this.unsubscribeWrappedObject(); if (!newValue || typeof newValue !== 'object') { // undefined, null, simple type: // nothing to subscribe to in case of new value undefined || null || simple type this.wrappedValue_ = newValue; } else if (newValue instanceof SubscribableAbstract) { stateMgmtConsole.propertyAccess(`${this.debugInfo()}: setValueInternal: new value is an SubscribableAbstract, subscribing to it.`); this.wrappedValue_ = newValue; (this.wrappedValue_ as unknown as SubscribableAbstract).addOwningProperty(this); } else if (ObservedObject.IsObservedObject(newValue)) { stateMgmtConsole.propertyAccess(`${this.debugInfo()}: setValueInternal: new value is an ObservedObject already`); ObservedObject.addOwningProperty(newValue, this); this.shouldInstallTrackedObjectReadCb = TrackedObject.needsPropertyReadCb(newValue); this.wrappedValue_ = newValue; } else { stateMgmtConsole.propertyAccess(`${this.debugInfo()}: setValueInternal: new value is an Object, needs to be wrapped in an ObservedObject.`); this.wrappedValue_ = ObservedObject.createNew(newValue, this); this.shouldInstallTrackedObjectReadCb = TrackedObject.needsPropertyReadCb(this.wrappedValue_); } stateMgmtProfiler.end(); return true; }

这里面实际上主要做了以下三件事:

  1. 进行内部状态值更新,并设置回调

  2. 绑定回调方,比如当属性发生通知的时候,通过回调告诉回调方

  3. 把UI设置为脏处理,应用于后面UI的刷新流程

第一件事,进行内部状态值更新,这里其实很容易理解,就是把set后的数值记录下来,这里其实是通过wrappedValue_记录的,setValueInternal里面会把wrappedValue_更新为最后一次set的值。

第二件事,绑定回调方。这里先通过this.unsubscribeWrappedObject(); 把旧的值解除绑定。这里面判断了是SubscribableAbstract还是ObservedObject 进行单独的处理

kotlin
复制代码
private unsubscribeWrappedObject() { if (this.wrappedValue_) { if (this.wrappedValue_ instanceof SubscribableAbstract) { (this.wrappedValue_ as SubscribableAbstract).removeOwningProperty(this); } else { ObservedObject.removeOwningProperty(this.wrappedValue_, this); // make sure the ObservedObject no longer has a read callback function // assigned to it ObservedObject.unregisterPropertyReadCb(this.wrappedValue_); } } }

最后根据监听的类型不同分别调用不同的方法把ViewPU注册进行,后续当属性发生改变的时候,ViewPU就能够得知,这里面可以看到,ViewPU它实现了IPropertySubscriber接口。

scala
复制代码
abstract class ViewPU extends NativeViewPartialUpdate implements IViewPropertiesChangeSubscriber interface IViewPropertiesChangeSubscriber extends IPropertySubscriber { // ViewPU get informed when View variable has changed // informs the elmtIds that need update upon variable change viewPropertyHasChanged(varName: PropertyInfo, dependentElmtIds: Set<number>): void ; }

最后,到了最关键的UI刷新流程了,setValueInternal返回true的时候,就会执行notifyObjectValueAssignment进行回调,最终分为两个分支:如果class 里面没有@Track装饰器修饰的变量,则通过notifyPropertyHasChangedPU方法进行刷新(所以依赖这个class的UI都会被刷新),如果有的话,则通过notifyTrackedObjectPropertyHasChanged进行刷新(只依赖@Track装饰器修饰的变量的UI才会刷新)

kotlin
复制代码
if (this.setValueInternal(newValue)) { TrackedObject.notifyObjectValueAssignment(/* old value */ oldValue, /* new value */ this.wrappedValue_, this.notifyPropertyHasChangedPU.bind(this), this.notifyTrackedObjectPropertyHasChanged.bind(this)); }

这里面涉及到了一个ArkUI中对于渲染效率的问题,我们重点看一下这里!

ArkUI的状态管理优化之路

在api 9 时,开发者能够以来的状态刷新装饰器一般有限,比如@State,@Link 这些装饰器。它们都有一个缺陷,就是当依赖是一个类对象时,往往会导致不必要的刷新,我们来看一下例子代码

typescript
复制代码
@Component struct StateCase{ @State showRow:TestTrack = new TestTrack() build(){ Row(){ if (this.showRow.param1){ Row(){ Text("i am row") } }else{ Column(){ Text("i am colomn") } } // 冗余渲染,因为param2没有改变但是也会随着button点击发生重建 Text(this.showRow.param2?"我是Text":"").width(this.param2Text()) Button("点我").onClick(()=>{ this.showRow.param1 = !this.showRow.param1 }) } } 发生渲染时 param2Text(){ console.log("发生了渲染") return 100 } } class TestTrack{ param1:boolean = true param2:boolean = true }

当我们多次点击了button之后,我们可以观察到“发生了渲染”log输出了多变,这是因为Text发生了重建时会进行函数的调用

这种现象被称为冗余渲染,即是param2 没有改动也会因为param1的修改导致后续以来param2的组件发生了重新绘制。

这里我们再回到ObservedPropertyPU的刷新流程:

kotlin
复制代码
TrackedObject.notifyObjectValueAssignment(/* old value */ oldValue, /* new value */ this.wrappedValue_, this.notifyPropertyHasChangedPU.bind(this), this.notifyTrackedObjectPropertyHasChanged.bind(this)); }

当属性发生改变的时候,会通过notifyObjectValueAssignment方法进行分类,一类是没有@Track 装饰器修饰的函数处理,另一类是有@Track 装饰器修饰的处理。

typescript
复制代码
public static notifyObjectValueAssignment(obj1: Object, obj2: Object, notifyPropertyChanged: () => void, // notify as assignment (none-optimised) notifyTrackedPropertyChange: (propName) => void): boolean { // 默认处理,依赖class的属性控件都调用notifyPropertyChanged if (!obj1 || !obj2 || (typeof obj1 !== 'object') || (typeof obj2 !== 'object') || (obj1.constructor !== obj2.constructor) || TrackedObject.isCompatibilityMode(obj1)) { stateMgmtConsole.debug(`TrackedObject.notifyObjectValueAssignment notifying change as assignment (non-optimised)`) notifyPropertyChanged(); return false; } // 有@Track 装饰器 处理,通过属性变量查找到对应的属性,然后只刷新依赖属性的UI stateMgmtConsole.debug(`TrackedObject.notifyObjectValueAssignment notifying actually changed properties (optimised)`) const obj1Raw = ObservedObject.GetRawObject(obj1); const obj2Raw = ObservedObject.GetRawObject(obj2); let shouldFakePropPropertyBeNotified: boolean = false; Object.keys(obj2Raw) .forEach(propName => { // Collect only @Track'ed changed properties if (Reflect.has(obj1Raw, `${TrackedObject.___TRACKED_PREFIX}${propName}`) && (Reflect.get(obj1Raw, propName) !== Reflect.get(obj2Raw, propName))) { stateMgmtConsole.debug(` ... '@Track ${propName}' value changed - notifying`); notifyTrackedPropertyChange(propName); shouldFakePropPropertyBeNotified = true; } else { stateMgmtConsole.debug(` ... '${propName}' value unchanged or not @Track'ed - not notifying`); } }); }

从上面可以看到,默认情况下,我们isCompatibilityMode 都会返回true,从而直接走到刷新流程,即调用参数为notifyPropertyChanged的方法,即我们外部传入的notifyPropertyHasChangedPU方法

typescript
复制代码
public static isCompatibilityMode(obj: Object): boolean { return !obj || (typeof obj !== "object") || !Reflect.has(obj, TrackedObject.___IS_TRACKED_OPTIMISED); }

notifyPropertyHasChangedPU 方法是一个全量刷新方法,所有依赖了class的ViewPU属性都会进行重新渲染,即调用viewPropertyHasChanged方法

kotlin
复制代码
protected notifyPropertyHasChangedPU() { stateMgmtProfiler.begin("ObservedPropertyAbstractPU.notifyPropertyHasChangedPU"); stateMgmtConsole.debug(`${this.debugInfo()}: notifyPropertyHasChangedPU.`) if (this.owningView_) { if (this.delayedNotification_ == ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay) { // send viewPropertyHasChanged right away this.owningView_.viewPropertyHasChanged(this.info_, this.dependentElmtIdsByProperty_.getAllPropertyDependencies()); } else { // mark this @StorageLink/Prop or @LocalStorageLink/Prop variable has having changed and notification of viewPropertyHasChanged delivery pending this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending; } } this.subscriberRefs_.forEach((subscriber) => { if (subscriber) { if ('syncPeerHasChanged' in subscriber) { (subscriber as unknown as PeerChangeEventReceiverPU<T>).syncPeerHasChanged(this); } else { stateMgmtConsole.warn(`${this.debugInfo()}: notifyPropertyHasChangedPU: unknown subscriber ID 'subscribedId' error!`); } } }); stateMgmtProfiler.end(); }

viewPropertyHasChanged 这里终于来到我们之前说过的UI渲染逻辑,它会在内部调用markNeedUpdate方法把当前UI节点设置为脏状态,同时如果有@Watch 装饰器修饰的方法,在这个时候也会被回调。@Watch 装饰器也是api9 以上新增的方法,用于监听某个属性刷新然后触发方法调用。

typescript
复制代码
viewPropertyHasChanged(varName: PropertyInfo, dependentElmtIds: Set<number>): void { stateMgmtProfiler.begin("ViewPU.viewPropertyHasChanged"); stateMgmtTrace.scopedTrace(() => { if (this.isRenderInProgress) { stateMgmtConsole.applicationError(`${this.debugInfo__()}: State variable '${varName}' has changed during render! It's illegal to change @Component state while build (initial render or re-render) is on-going. Application error!`); } this.syncInstanceId(); if (dependentElmtIds.size && !this.isFirstRender()) { if (!this.dirtDescendantElementIds_.size && !this.runReuse_) { // mark ComposedElement dirty when first elmtIds are added // do not need to do this every time 进行标记,进入UI刷新的流程 this.markNeedUpdate(); } stateMgmtConsole.debug(`${this.debugInfo__()}: viewPropertyHasChanged property: elmtIds that need re-render due to state variable change: ${this.debugInfoElmtIds(Array.from(dependentElmtIds))} .`) for (const elmtId of dependentElmtIds) { if (this.hasRecycleManager()) { this.dirtDescendantElementIds_.add(this.recycleManager_.proxyNodeId(elmtId)); } else { this.dirtDescendantElementIds_.add(elmtId); } } stateMgmtConsole.debug(` ... updated full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`) } else { stateMgmtConsole.debug(`${this.debugInfo__()}: viewPropertyHasChanged: state variable change adds no elmtIds for re-render`); stateMgmtConsole.debug(` ... unchanged full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`) } 回调@Watch 装饰器修饰方法 let cb = this.watchedProps.get(varName) if (cb) { stateMgmtConsole.debug(` ... calling @Watch function`); cb.call(this, varName); } this.restoreInstanceId(); }, "ViewPU.viewPropertyHasChanged", this.constructor.name, varName, dependentElmtIds.size); stateMgmtProfiler.end(); }

这里也就解释了,为什么@State 修饰的class变量,会产生冗余渲染的原因,因为所有依赖的ViewPU都会被标记重建。

回到上文,如果isCompatibilityMode返回false,即Reflect.has(obj, TrackedObject.___IS_TRACKED_OPTIMISED)为true的情况下,证明当前对象有@Track 属性,因此做的事情也比较简单,就是找到@Track 装饰器修饰的属性,并刷新只依赖了属性的ViewPU(getTrackedObjectPropertyDependencies 方法获取)

kotlin
复制代码
protected notifyTrackedObjectPropertyHasChanged(changedPropertyName : string) : void { stateMgmtProfiler.begin("ObservedPropertyAbstract.notifyTrackedObjectPropertyHasChanged"); stateMgmtConsole.debug(`${this.debugInfo()}: notifyTrackedObjectPropertyHasChanged.`) if (this.owningView_) { if (this.delayedNotification_ == ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay) { // send viewPropertyHasChanged right away this.owningView_.viewPropertyHasChanged(this.info_, this.dependentElmtIdsByProperty_.getTrackedObjectPropertyDependencies(changedPropertyName, "notifyTrackedObjectPropertyHasChanged")); } else { // mark this @StorageLink/Prop or @LocalStorageLink/Prop variable has having changed and notification of viewPropertyHasChanged delivery pending this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending; } } this.subscriberRefs_.forEach((subscriber) => { if (subscriber) { if ('syncPeerTrackedPropertyHasChanged' in subscriber) { (subscriber as unknown as PeerChangeEventReceiverPU<T>).syncPeerTrackedPropertyHasChanged(this, changedPropertyName); } else { stateMgmtConsole.warn(`${this.debugInfo()}: notifyTrackedObjectPropertyHasChanged: unknown subscriber ID 'subscribedId' error!`); } } }); stateMgmtProfiler.end(); }

至此,我们完成了整个@State 装饰器内部的逻辑分析,以及在api11会存在的@Track @Watch装饰器的流程,这些新增的装饰器都是为了解决ArkTS中存在的局限而产生。合理运用不同的装饰器,才能把ArkUI的性能发挥得更好

回到上面的例子,如果param2不需要被param1刷新,我们只需要使用@Track装饰器标记param1即可,因此后续变化只会追踪param1的变化。更多例子可以观看这里 @Track

ini
复制代码
class TestTrack{ @Track param1:boolean = true param2:boolean = true }

ObservedPropertyPU get方法

ObservedPropertyPU的get方法比较简单,核心逻辑就是返回set方法中设置的最新值,即wrappedValue_

kotlin
复制代码
public get(): T { stateMgmtProfiler.begin("ObservedPropertyPU.get"); stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get`); this.recordPropertyDependentUpdate(); if (this.shouldInstallTrackedObjectReadCb) { stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get: @Track optimised mode. Will install read cb func if value is an object`); ObservedObject.registerPropertyReadCb(this.wrappedValue_, this.onOptimisedObjectPropertyRead.bind(this)); } else { stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get: compatibility mode. `); } stateMgmtProfiler.end(); return this.wrappedValue_; }

在后续UI重建会回调ViewPU 在 initialRender方法时,调用observeComponentCreation 放入的更新函数,此时就能够拿到最新的变量了,本例子就是showRow变量。

javascript
复制代码
this.observeComponentCreation((elmtId, isInitialRender) => { ... if (this.showRow) { this.ifElseBranchUpdateFunction(0, () => { this.observeComponentCreation((elmtId, isInitialRender) => {

总结

通过学习ArkUI中的状态管理,我们应该对ArkTS中的状态装饰器有了更加深入的理解,正是有了这些装饰器背后的运行机制,才能让开发者构建出低成本且丰富多彩的响应式UI框架。状态管理驱动UI刷新是ArkUI中面向开发者最核心的一部分,希望本文对你有所帮助。

源文:ArkUI Engine - 探索状态管理装饰器的实现

如有侵权请联系站点删除!

技术合作服务热线,欢迎来电咨询!