本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
ArkUI 状态管理
文章介绍
从前几篇文章中,我们了解到ArkUI中针对UI进行刷新处理流程,对于应用开发者来说,ArkUI把驱动UI刷新的一系列操作通过状态管理装饰器,比如@State,@Prop等暴露给开发者,通过引用这些装饰器修改的变量,我们能够实现自动的UI刷新处理。
值得注意的是,本章会涉及到api 9以上的内容,比如api 11,这些内容虽然在华为官网还没有暴露给普通开发者,但是我们可以通过open harmony docs 中提取查看这些内容,比如@Track 装饰器。 因此如果大家想要提前了解鸿蒙Next的api,即便公司没有和华为签约,我们也还是能够通过open harmony docs去查看更高版本的内容。
本篇我们讲进入状态管理相关内容的学习,通过针对状态管理的学习,我们能够了解到以下知识点:
- 了解常见的状态管理,比如@State装饰器 是如何进行状态刷新
- 鸿蒙api 9 class全量刷新导致的问题以及ArkUI后续优化的实现原理,比如@Track 装饰器
- 了解到属性刷新驱动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;
}
这里面实际上主要做了以下三件事:
-
进行内部状态值更新,并设置回调
-
绑定回调方,比如当属性发生通知的时候,通过回调告诉回调方
-
把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中面向开发者最核心的一部分,希望本文对你有所帮助。