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

本文为稀土掘金技术社区首发签约文章,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中面向开发者最核心的一部分,希望本文对你有所帮助。

相关推荐
蓝枫amy2 小时前
HarmonyOS快速入门
华为·harmonyos
水瓶丫头站住4 小时前
安卓APP如何适配不同的手机分辨率
android·智能手机
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
xvch5 小时前
Kotlin 2.1.0 入门教程(五)
android·kotlin
hunter2062066 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb6 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角6 小时前
CSS 颜色
前端·css
九酒6 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔7 小时前
HTML5 新表单属性详解
前端·html·html5
程序猿阿伟7 小时前
《探秘鸿蒙Next:如何保障AI模型轻量化后多设备协同功能一致》
人工智能·华为·harmonyos