鸿蒙ACE-V2状态分析@ObservedV2、@Trace

76 阅读11分钟

状态管理现状

IDE 5.0.3.402

developer.huawei.com/consumer/cn…

现有状态管理框架使用代理观察数据,当创建一个状态变量时,同时也创建了一个数据代理观察者。该观察者可感知代理变化,但无法感知实际数据变化,因此在使用上有如下限制:

  • 状态变量不能独立于UI存在,同一个数据被多个视图代理时,在其中一个视图的更改不会通知其他视图更新。
  • 只能感知对象属性第一层的变化,无法做到深度观测和深度监听。
  • 更改对象中属性以及更改数组中元素的场景下存在冗余更新的问题。
  • 装饰器间配合使用限制多,不易用。组件中没有明确状态变量的输入与输出,不利于组件化。

image.png

状态管理(推荐)介绍

新的状态管理框架将观察能力增强到数据本身,数据本身就是可观察的,更改数据会触发相应的视图的更新。相较于当前的状态管理框架,新框架有如下优点:

  • 状态变量独立于UI,更改数据会触发相应视图的更新。

  • 支持对象的深度观测和深度监听,且深度观测机制不影响观测性能。

  • 支持对象中属性级精准更新及数组中元素的最小化更新

  • 装饰器易用性高、拓展性强,在组件中明确输入与输出,有利于组件化。

image.png

@ObservedV2装饰器和@Trace装饰器:类属性变化观测

developer.huawei.com/consumer/cn…

转换前代码

/**
 *
 * TraceChildCmpt.ets
 * Created by unravel on 2024/5/3
 * @abstract
 */

let nextId: number = 0;

@ObservedV2
class Person {
  // 基本类型
  @Trace age: number = 0;

  constructor(age: number) {
    this.age = age;
  }
}

@ObservedV2
class Info {
  id: number = 0;
  // 对象数组
  @Trace personList: Person[] = [];

  constructor() {
    this.id = nextId++;
    this.personList = [new Person(0), new Person(1), new Person(2)];
  }
}

@Component
export struct TraceChildCmpt {
  info: Info = new Info();

  build() {
    Column() {
      Text(`length: ${this.info.personList.length}`)
        .fontSize(40)
      Divider()
      if (this.info.personList.length >= 3) {
        Text(`${this.info.personList[0].age}`)
          .fontSize(40)
          .onClick(() => {
            this.info.personList[0].age++;
          })

        Text(`${this.info.personList[1].age}`)
          .fontSize(40)
          .onClick(() => {
            this.info.personList[1].age++;
          })

        Text(`${this.info.personList[2].age}`)
          .fontSize(40)
          .onClick(() => {
            this.info.personList[2].age++;
          })
      }

      Divider()

      ForEach(this.info.personList, (item: Person, index: number) => {
        Text(`${index} ${item.age}`)
          .fontSize(40)
      })
    }
  }
}

转换后代码

if (!("finalizeConstruction" in ViewPU.prototype)) {
    Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
}
interface TraceChildCmpt_Params {
    info?: Info;
}
/**
 *
 * TraceChildCmpt.ets
 * Created by unravel on 2024/5/3
 * @abstract
 */
let nextId: number = 0;
@ObservedV2
class Person {
    // 基本类型
    @Trace
    age: number = 0;
    constructor(age: number) {
        this.age = age;
    }
}
@ObservedV2
class Info {
    id: number = 0;
    // 对象数组
    @Trace
    personList: Person[] = [];
    // @Computed
    // get varName() {
    //   return this.personList[0] + this.personList[1]
    // }
    constructor() {
        this.id = nextId++;
        this.personList = [new Person(0), new Person(1), new Person(2)];
    }
}
export class TraceChildCmpt extends ViewPU {
    constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
        super(parent, __localStorage, elmtId, extraInfo);
        if (typeof paramsLambda === "function") {
            this.paramsGenerator_ = paramsLambda;
        }
        this.info = new Info();
        this.setInitiallyProvidedValue(params);
        this.finalizeConstruction();
    }
    setInitiallyProvidedValue(params: TraceChildCmpt_Params) {
        if (params.info !== undefined) {
            this.info = params.info;
        }
    }
    updateStateVars(params: TraceChildCmpt_Params) {
    }
    purgeVariableDependenciesOnElmtId(rmElmtId) {
    }
    aboutToBeDeleted() {
        SubscriberManager.Get().delete(this.id__());
        this.aboutToBeDeletedInternal();
    }
    private info: Info;
    // @Monitor('varName.obj', 'varName.obj.proA', 'varName2') onChange(m : IMonitor) : void { ... }
    initialRender() {
        this.observeComponentCreation2((elmtId, isInitialRender) => {
            Column.create();
            Column.debugLine("entry/src/main/ets/pages/statev2/TraceChildCmpt.ets(44:5)");
        }, Column);
        this.observeComponentCreation2((elmtId, isInitialRender) => {
            Text.create(`length: ${this.info.personList.length}`);
            Text.debugLine("entry/src/main/ets/pages/statev2/TraceChildCmpt.ets(45:7)");
            Text.fontSize(40);
        }, Text);
        Text.pop();
        this.observeComponentCreation2((elmtId, isInitialRender) => {
            Divider.create();
            Divider.debugLine("entry/src/main/ets/pages/statev2/TraceChildCmpt.ets(47:7)");
        }, Divider);
        this.observeComponentCreation2((elmtId, isInitialRender) => {
            If.create();
            if (this.info.personList.length >= 3) {
                this.ifElseBranchUpdateFunction(0, () => {
                    this.observeComponentCreation2((elmtId, isInitialRender) => {
                        Text.create(`${this.info.personList[0].age}`);
                        Text.debugLine("entry/src/main/ets/pages/statev2/TraceChildCmpt.ets(49:9)");
                        Text.fontSize(40);
                        Text.onClick(() => {
                            this.info.personList[0].age++;
                        });
                    }, Text);
                    Text.pop();
                    this.observeComponentCreation2((elmtId, isInitialRender) => {
                        Text.create(`${this.info.personList[1].age}`);
                        Text.debugLine("entry/src/main/ets/pages/statev2/TraceChildCmpt.ets(55:9)");
                        Text.fontSize(40);
                        Text.onClick(() => {
                            this.info.personList[1].age++;
                        });
                    }, Text);
                    Text.pop();
                    this.observeComponentCreation2((elmtId, isInitialRender) => {
                        Text.create(`${this.info.personList[2].age}`);
                        Text.debugLine("entry/src/main/ets/pages/statev2/TraceChildCmpt.ets(61:9)");
                        Text.fontSize(40);
                        Text.onClick(() => {
                            this.info.personList[2].age++;
                        });
                    }, Text);
                    Text.pop();
                });
            }
            else {
                this.ifElseBranchUpdateFunction(1, () => {
                });
            }
        }, If);
        If.pop();
        this.observeComponentCreation2((elmtId, isInitialRender) => {
            Divider.create();
            Divider.debugLine("entry/src/main/ets/pages/statev2/TraceChildCmpt.ets(68:7)");
        }, Divider);
        this.observeComponentCreation2((elmtId, isInitialRender) => {
            ForEach.create();
            const forEachItemGenFunction = (_item, index: number) => {
                const item = _item;
                this.observeComponentCreation2((elmtId, isInitialRender) => {
                    Text.create(`${index} ${item.age}`);
                    Text.debugLine("entry/src/main/ets/pages/statev2/TraceChildCmpt.ets(71:9)");
                    Text.fontSize(40);
                }, Text);
                Text.pop();
            };
            this.forEachUpdateFunction(elmtId, this.info.personList, forEachItemGenFunction, undefined, true, false);
        }, ForEach);
        ForEach.pop();
        Column.pop();
    }
    rerender() {
        this.updateDirtyElements();
    }
}

@Trace、@ObservedV2

转换前后

可以看到转换前后没有任何的变化。那么@Trace是怎么驱动UI更新的呢? 既然转换成TS都没有什么变化,那一定是在编译期做了什么

image.png

@ObservedV2装饰器

function ObservedV2(BaseClass: T): T

我们在 arkui_ace_engine 源码中查找@ObservedV2装饰器定义。

这里提及一点, @ObservedV2和@Trace之前的装饰符叫@observed和@track。所以在源码中看到@observed可以等价理解为@ObservedV2,@track等价理解为@Trace

可以看到装饰器的实际实现是ObservedV2Internal函数

image.png

function observedV2Internal(BaseClass: T): T

image.png

AsyncAddComputedV2

static addComputed(target: Object, name: string): void

如果 computedVars的长度为0 创建一个微任务运行AsyncAddComputedV2.run方法,然后将{target:target, name:name} 添加到computedVars中

因为微任务会在这个宏任务完成之后调用,到那时 computedVars 里面至少会有本次添加的{target:target, name:name}

我们看到run方法里遍历computedVars,使用每一个computeVar调用了 ObserveV2.getObserve().constructComputed(computedVar.target, computedVar.name));

image.png

ObservedV2

public static getObserve(): ObserveV2

一个ObserveV2的单例,没啥好说的

image.png

public constructComputed(owningObject: Object, owningObjectName: string): void

遍历owningObject[computedProp]属性的每一项,然后构建一个ComputedV2并调用InitRun, 然后把ComputedV2挂载到owningObject的COMPUTED_REFS属性上

owningObject[computedProp]这个属性里面的值又是哪里来的呢?我们后面再看一下

我们看一下COMPUTED_REFS

public static readonly COMPUTED_REFS = Symbol('__computed_refs') ;  依然是一个独一无二的Symbol属性

image.png

owningObject[computedProp] 怎么来的?

我们搜索 Symbol.for(ComputedV2.COMPUTED_PREFIX,发现只有一处代码

Computed装饰器

const Computed = (target: Object, propertyKey: string, descriptor: PropertyDescriptor): void

这是一个属性装饰器,对应的就是我们可以在ComponentV2中使用的@Computed

let watchProp = Symbol.for(ComputedV2.COMPUTED_PREFIX + target.constructor.name);

watchProp是通过固定前缀+类名创建的一个Symbol。接着给target增加了一个watchProp属性,这个属性的值是一个对象

watchProp对象里面每个属性对应@Component装饰的计算函数

image.png

ComputedV2

public InitRun(): number

这个方法内部将原有属性重新定义为一个getter,又添加了另外一个属性用于存储实际的值

在外部访问原有属性时就会走到getter里面,getter里面调用了ObserveV2.getObserve().addRefObserveV2.autoProxyObject

image.png

ObserveV2

public addRef(target: object, attrName: string): void

添加对当前this的引用,并在target上存储了两个独一无二的属性ObserveV2.SYMBOL_REFObserveV2.ID_REFS

ObserveV2.SYMBOL_REF存储的key为属性名,value 为依赖这个属性的组件的id组成的集合

ObserveV2.ID_REFS存储的key为组件id,value为这个组件依赖的属性名组成的集合

最后还将组件使用的状态变量的引用存储在了一个WeakSet中

那么就形成了下面的关系

  1. id2targets是一个以id为key,和id关联的targets的集合为value的映射

  2. 每个target中又维护了两个映射

    1. id_refs: 以id为key,和id关联的属性名的集合为value的映射
    2. symbol_refs:以属性名为key,和该属性关联的组件id的集合为value的映射

image.png

stackOfRenderedComponents_栈结构维护

我们看一下这个属性的定义。从定义中我们知道这是一个栈形式的数据结构

private stackOfRenderedComponents_ : StackOfRenderedComponents = new StackOfRenderedComponents();

然后看一下ObserveV2中和这个栈的入栈和弹栈的方法有哪些。

image.png

public static autoProxyObject(target: Object, key: string | symbol): any

这个方法里判断了一些类型,然后保证了对应属性必然是一个Proxy,具体的代理动作是通过ObserveV2.arraySetMapProxy完成的

image.png

public static readonly arraySetMapProxy
// 造成数组长度改变的方法有这些
private static readonly arrayLengthChangingFunctions = new Set(['push', 'pop', 'shift', 'splice', 'unshift']);
// 修改数组内容的方法有这些
  private static readonly arrayMutatingFunctions = new Set(['copyWithin', 'fill', 'reverse', 'sort']);
// 操作Date的方法有这些
  private static readonly dateSetFunctions = new Set(['setFullYear', 'setMonth', 'setDate', 'setHours', 'setMinutes',
    'setSeconds', 'setMilliseconds', 'setTime', 'setUTCFullYear', 'setUTCMonth', 'setUTCDate', 'setUTCHours',
    'setUTCMinutes', 'setUTCSeconds', 'setUTCMilliseconds']);

  public static readonly arraySetMapProxy = {
    get(
      target: any,
      key: string | symbol,
      receiver: any
    ): any {
    // 如果key的类型是symbol
      if (typeof key === 'symbol') {
    // 判断是否是获取迭代器
        if (key === Symbol.iterator) {
      // 调用fireChange,调用addRef
          ObserveV2.getObserve().fireChange(target, ObserveV2.OB_MAP_SET_ANY_PROPERTY);
          ObserveV2.getObserve().addRef(target, ObserveV2.OB_LENGTH);
          return (...args): any => target[key](...args);
        } else {
          // 判断key如果是要获取target的key,直接返回target,否则返回key对应的值target[key]
          return key === ObserveV2.SYMBOL_PROXY_GET_TARGET ? target : target[key];
        }
      }
      // 如果是获取长度
      if (key === 'size') {
        // 添加长度的引用
        ObserveV2.getObserve().addRef(target, ObserveV2.OB_LENGTH);
        return target.size;
      }

    // 递归处理target上key属性的读取
      let ret = ObserveV2.autoProxyObject(target, key);
      if (typeof (ret) !== 'function') {
    // 如果不是函数类型,添加对key属性的引用
        ObserveV2.getObserve().addRef(target, key);
        return ret;
      }
      // 如果数据源是数组Array
      if (Array.isArray(target)) {
        if (ObserveV2.arrayMutatingFunctions.has(key)) {
          return function (...args): any {
            ret.call(target, ...args);
            ObserveV2.getObserve().fireChange(target, ObserveV2.OB_LENGTH);
          // 调用原始函数后,返回代理对象本身,达到链式调用的目的。这样每次返回的都是代理对象自身
            // returning the 'receiver(proxied object)' ensures that when chain calls also 2nd function call
            // operates on the proxied object.
            return receiver;
          };
        } else if (ObserveV2.arrayLengthChangingFunctions.has(key)) {
          return function (...args): any {
      // 调用原有函数,然后将结果返回
            const result = ret.call(target, ...args);
            ObserveV2.getObserve().fireChange(target, ObserveV2.OB_LENGTH);
            return result;
          };
        } else {
          return ret.bind(receiver);
        }
      }
    // 如果是Date对象
      if (target instanceof Date) {
        if (ObserveV2.dateSetFunctions.has(key)) {
          return function (...args): any {
      // 调用原有函数
            // execute original function with given arguments
            let result = ret.call(this, ...args);
            ObserveV2.getObserve().fireChange(target, ObserveV2.OB_DATE);
            return result;
            // bind 'this' to target inside the function
          }.bind(target);
        } else {
          ObserveV2.getObserve().addRef(target, ObserveV2.OB_DATE);
        }
        return ret.bind(target);
      }
    // 如果是Set或者Map对象
      if (target instanceof Set || target instanceof Map) {
    // 如果是调用has方法,判断在不在集合内
        if (key === 'has') {
          return (prop): boolean => {
            const ret = target.has(prop);
            if (ret) {
              ObserveV2.getObserve().addRef(target, prop);
            } else {
              ObserveV2.getObserve().addRef(target, ObserveV2.OB_LENGTH);
            }
            return ret;
          };
        }
      // 如果是删除某个key
        if (key === 'delete') {
          return (prop): boolean => {
            if (target.has(prop)) {
              ObserveV2.getObserve().fireChange(target, prop);
              ObserveV2.getObserve().fireChange(target, ObserveV2.OB_LENGTH);
              return target.delete(prop);
            } else {
              return false;
            }
          };
        }
        // 如果是清空所有的keys
        if (key === 'clear') {
          return (): void => {
            if (target.size > 0) {
              target.forEach((_, prop) => {
                ObserveV2.getObserve().fireChange(target, prop.toString());
              });
              ObserveV2.getObserve().fireChange(target, ObserveV2.OB_LENGTH);
              ObserveV2.getObserve().addRef(target, ObserveV2.OB_MAP_SET_ANY_PROPERTY);
              target.clear();
            }
          };
        }
        // 如果是获取keys、values或者enteries
        if (key === 'keys' || key === 'values' || key === 'entries') {
          return (): any => {
            ObserveV2.getObserve().addRef(target, ObserveV2.OB_MAP_SET_ANY_PROPERTY);
            ObserveV2.getObserve().addRef(target, ObserveV2.OB_LENGTH);
            return target[key]();
          };
        }
      }
      // 如果是Set
      if (target instanceof Set) {
        return key === 'add' ?
        (val): any => {
          ObserveV2.getObserve().fireChange(target, val.toString());
          ObserveV2.getObserve().fireChange(target, ObserveV2.OB_MAP_SET_ANY_PROPERTY);
          if (!target.has(val)) {
            ObserveV2.getObserve().fireChange(target, ObserveV2.OB_LENGTH);
            target.add(val);
          }
          // return proxied This
          return receiver;
        } : (typeof ret === 'function')
          ? ret.bind(target) : ret;
      }
     // 如果是Map
      if (target instanceof Map) {
        if (key === 'get') { // for Map
          return (prop): any => {
            if (target.has(prop)) {
              ObserveV2.getObserve().addRef(target, prop);
            } else {
              ObserveV2.getObserve().addRef(target, ObserveV2.OB_LENGTH);
            }
            return target.get(prop);
          };
        }
        if (key === 'set') { // for Map
          return (prop, val): any => {
            if (!target.has(prop)) {
              ObserveV2.getObserve().fireChange(target, ObserveV2.OB_LENGTH);
            } else if (target.get(prop) !== val) {
              ObserveV2.getObserve().fireChange(target, prop);
            }
            ObserveV2.getObserve().fireChange(target, ObserveV2.OB_MAP_SET_ANY_PROPERTY);
            target.set(prop, val);
            return receiver;
          };
        }
      }

      return (typeof ret === 'function') ? ret.bind(target) : ret;
    },

    set(
      target: any,
      key: string | symbol,
      value: any
    ): boolean {
    // 如果key的类型是symbol,判断如果不是设置代理对象的话,就直接进行赋值。这里是为了防止两次设置代理对象
      if (typeof key === 'symbol') {
        if (key !== ObserveV2.SYMBOL_PROXY_GET_TARGET) {
          target[key] = value;
        }
        return true;
      }
    // 值相同时,不需要重新设置,是为了避免无谓的刷新
      if (target[key] === value) {
        return true;
      }
      target[key] = value;
    // 触发状态变量改变的回调
      ObserveV2.getObserve().fireChange(target, key.toString());
      return true;
    }
  };
public fireChange(target: object, attrName: string): void

将依赖attrName属性的组件id,分别添加到对应的Set集合中。

elmtIdsChanged_变化的组件ids、

computedPropIdsChange_变化的计算属性ids、

monitorIdsChanged_变化的监听ids

image.png

从ArkUIInspector看组件id

通过ArkUI Inspector我们看到组件的id是递增的。还记的我们之前看到的ViewPU是通过create,pop按照栈的形式组成组件树的嘛。

这里也能看出一点皮毛。同一容器下的兄弟组件id是连续递增的。比如Column1321下的组件。兄弟组件不连续的一般是由于这些组件是通过分支控制展示与否的组件

image.png

image.png

private updateDirty(): void

设置了标记为之后,最终调用了updateDirty2

image.png

private updateDirty2(): void

image.png

private updateDirtyComputedProps(computed: Array): void

首先更新计算属性,因为计算属性可能会修改正常状态变量的值

image.png

ComputedV2  public fireChange(): void

最终调用到了ComputedV2的fireChange,这个里面会取出target的SYMBOL_REFS属性,进而取出依赖prop属性的组件id的集合,添加进 computedPropIdsChanged_。然后更新

image.png

private updateDirtyMonitors(monitors: Set): void

和 updateDirtyComputedProps 很类似,遍历数组调用notifyChange

image.png

MonitorV2 public notifyChange(): void

image.png

private updateUINodes(elmtIds: Array): void

遍历组件id,调用组件上的uiNodeNeedUpdateV3,

image.png

ViewPU public uiNodeNeedUpdateV3(elmtId: number): void

image.png

ComputedV2

private observeObjectAccess(): Object | undefined

这里调用了startRecordDependencies将this和computedId_传入。存入到了ObserveV2的 stackOfRenderedComponents_中

image.png

AsyncAddMonitorV2

static addMonitor(target: any, name: string): void

如果没有添加过需要监听的对象,创建一个微任务运行run方法。

我们看到run方法里又调用了ObserveV2.getObserve().constructMonitor(item[0], item[1])

ObserveV2是一个单例,咱们上面已经知道了

image.png

ObserveV2

public constructMonitor(owningObject: Object, owningObjectName: string): void

我们看到首先声明了一个独一无二的Symbol变量,判断owningObject的watchProp属性有没有值。

如果有值,遍历这个属性的值,每一项的值都被用于构建一个monitor并把monitor挂载到owningObject的MONITOR_REFS属性上

public static readonly MONITOR_REFS = Symbol('__monitor_refs'); 依然是一个独一无二的Symbol属性

image.png

owningObject[watchProp]哪里来的值?

我们搜索 Symbol.for(MonitorV2.WATCH_PREFIX,发现只有一处赋值的地方

通过注释,我们知道@Monitor是在@ObserveV2装饰器里实现的装饰器,用于监听属性路径的访问。

Monitor装饰器

这是一个属性装饰器,具体的操作是将所有的@Monitor添加到对象的target[watchProp]属性上,这个属性又是一个对象。

image.png

MonitorV2

InitRun(): MonitorV2

InitRun里面调用了bindRun并将是否是首次渲染作为参数传入。 bindRun里面遍历了this.values_,然后依次调用item的setValue。只要有一个是dirty,ret就是dirty

image.png

@Trace装饰器

Trace装饰器最终的实现是通过trackInternal完成的。因为@Trace之前的名称是@track,所以代码里也有@track就不足为期了,我们只需要知道他俩是一个东西就行

image.png

const trackInternal

可以看到这个实现和@State的实现类似。也是将原始属性重写为getter和settter,同时声明一个新的属性用于存储原来属性的值,在getter和setter中操作这个新属性

image.png ObserveV2的addRef方法以及autoProxyObject方法 我们在上面已经看到过了

addRef给target增加了两个属性ID_REFSSYMBOL_REFS,这样就能通过属性查找使用这个属性的组件集合,也能通过组件查找组件使用的属性的集合

autoProxyObject则对属性的getter和setter进行了代理,这样属性变化的时候就可以调用fireChange将对应的组件id标记为dirty,从而驱动UI更新

图示

@Trace和@ObservedV2的成员组成图

image.png

参考资料

  1. wangdoc.com/es6/proxy
  2. wangdoc.com/es6/symbol#…
  3. gitee.com/openharmony…
  4. 声明式范式的语法编译转换,语法验证 https://gitee.com/openharmony/developtools_ace_ets2bundle