鸿蒙ACE-V2状态概览

141 阅读15分钟

Computed 装饰器

developer.huawei.com/consumer/cn…

Computed 是一个成员变量装饰器,他必须是一个getter属性,应用于ObservedV2装饰的类中或ComponentV2装饰的组件中

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

有@Computed装饰器的类的对象会有一个watchProp属性, 这个属性是一个对象。这个对象存储了@Computed所在类的所有被@Computed装饰的计算属性

watchProp属性里的每个属性是@Computed装饰的属性名,对应的值是@Computed装饰的计算函数

image.png

Monitor装饰器

developer.huawei.com/consumer/cn…

Monitor是一个方法装饰器,能应用于ObservedV2装饰的类中或ComponentV2装饰的组件中

有@Monitor装饰器的类的对象会有一个watchProp属性, 这个属性是一个对象。这个对象存储了@Monitor所在类的所有被@Monitor装饰的监听方法

watchProp属性对象里的每个属性是@Monitor中参数按照空格拼接的字符串,对应的值是@Monitor装饰的回调函数

image.png

Trace装饰器

developer.huawei.com/consumer/cn…

Trace是一个成员变量装饰器,应用于ObservedV2装饰的类中

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

image.png

const trackInternal

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

新声明的属性命名方式为:ObserveV2.OB_PREFIX+<原来的属性名>

在get方法中会保存对属性的引用,在set方法中只有旧值和新值不一样的时候才会触发UI的更新

image.png

ObserveV2的addRef方法以及autoProxyObject方法 我们后面在ObservedV2里会一起介绍

  1. addRef给target增加了两个属性ID_REFSSYMBOL_REFS,这两个属性都是对象类型的

    1. ID_REFS 这个对象中的每个属性,属性名为组件id,属性值为对应组件使用的状态变量组成的集合Set,这样就能 通过组件查找到组件使用的属性的集合
    2. SYMBOL_REFS 这个对象中的每个属性,属性名为状态变量名,属性值为使用这个状态变量的组件组成的集合Set,这样就能 通过属性查找到使用这个属性的组件集合
  2. autoProxyObject则对属性的getter和setter进行了代理

    1. 这样属性变化的时候就可以调用fireChange将对应的组件id标记为dirty,从而驱动UI更新

Local装饰器:组件内部状态

developer.huawei.com/consumer/cn…

Local是一个成员变量装饰器,应用于ComponentV2装饰的组件中

可以看到Local装饰器和Trace装饰器都是使用trackInternal实现。唯一的不同是Local和Trace在编译期做了不同的限制

Local被限制在只能在ComponentV2组件内使用

Trace被限制在只能在ObservedV2装饰的类中使用

image.png

Param装饰器

developer.huawei.com/consumer/cn…

Param是一个成员变量装饰器,应用于ComponentV2装饰的组件中

和@Trace、@Local的实现很相似。只是限制了@Param只能调用get不能调用set

image.png

@Param变量怎么进行初始化?

image.png

Once装饰器

developer.huawei.com/consumer/cn…

Once是一个成员变量装饰器,应用于ComponentV2装饰的组件中。它必须和@Param装饰器成对存在

可以看到Once装饰器很简单,只是在对象的元数据上添加了@once

image.png

public static addParamVariableDecoMeta(proto: Object, varName: string, deco?: string, deco2?: string): void

其中meta[varName] 只有不存在的时候才赋值了一个空对象。这是因为它需要和@Param联合使用

@param赋值给deco,@once赋值给deco2

image.png

为什么@Once只会在首次创建时赋值,后面更新就不会驱动UI更新了?

在VariableUtilsV3中有updateParam方法,用于@Param装饰的变量更新时调用

image.png

Event装饰器

developer.huawei.com/consumer/cn…

Event是一个成员变量装饰器,必须是一个函数类型,应用于ComponentV2装饰的组件中。

可以看到Event装饰器很简单,只是在对象的元数据上添加了@event

image.png

public static addVariableDecoMeta(proto: Object, varName: string, deco: string): void {

其中meta[varName] 每次会被重新赋值,并且只有一个deco属性,指向装饰器名称@event

image.png

Provider装饰器

developer.huawei.com/consumer/cn…

Provider 是一个成员变量装饰器,应用于ComponentV2装饰的组件中。

它除了给源对象添加了一些信息,最终的实现和@Trace、@Local一样都是调用的trackInternal

image.png

public static addProvideConsumeVariableDecoMeta(proto: Object, varName: string, aliasName: string, deco: '@Provider' | '@Consumer'): void {

可以看到元信息对象里面存储了两个属性

  1. 一个是原始属性对应的信息,包括装饰器名称和别名
  2. 一个是自定义的属性,包括原始变量名,别名,装饰器名称

image.png

private static metaAliasKey(aliasName: string, deco: '@Provider' | '@Consumer') : string

这里也给出了上面两个属性各自存储的格式

image.png

Consumer装饰器

developer.huawei.com/consumer/cn…

Consumer 是一个成员变量装饰器,应用于ComponentV2装饰的组件中。

和@Provider一样也添加了一些原信息。只不过因为它不是数据源,所以它不用调用trackInternal来初始化状态变量

image.png

@Consumer怎么和@Provider建立关系

ViewV2创建的时候会调用finalizeConstruction。finalizeConstruction内会调用ProviderConsumerUtilV2.setupConsumeVarsV2建立关系 

具体可见 鸿蒙ACE-ArkUI-Repeat、ViewV2源码

public static setupConsumeVarsV2(view: ViewV2): boolean

image.png

private static findProvider(view: ViewV2, aliasName: string): [ViewV2, string] | undefined

从最近的祖先开始递归往上查找有没有对应key的Provider值,没有就返回undefined,有就返回一个元组,元组第一个元素是视图,第二个元素是值

image.png

private static connectConsumer2Provider(consumeView: ViewV2, consumeVarName: string, provideView: ViewV2, provideVarName: string): void

和Provider中的值建立关系。

最终操作的值依然是祖先组件上的Provider值,只是在变化的时候通知了使用对应属性的组件进行更新

image.png

private static defineConsumerWithoutProvider(consumeView: ViewV2, consumeVarName: string): void

在祖先组件中没找到Provider对应的属性时,会走这个方法

创建存储属性并且进行观察和驱动UI的逻辑和trackInternal方法很类似

image.png

!!语法:双向绑定

developer.huawei.com/consumer/cn…

!!双向绑定语法,是一个语法糖方便开发者实现数据双向绑定,用于初始化子组件的@Param和@Event。其中@Event方法名需要声明为“$”+ @Param属性名

双向绑定语法糖可视为:

  1. Star({ value: this.value, $value: (val: number) => { this.value = val }})

本来我们传的是 {value:this.value!!}

被转换成了 { value: this.value!!, $value: value => { this.value = value; } }

image.png

ObservedV2装饰器

developer.huawei.com/consumer/cn…

ObservedV2是一个类装饰器,只能应用于class中

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

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

image.png

function observedV2Internal<T extends ConstructorV2>(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装饰的计算属性

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

image.png

ComputedV2

constructor(target: object, prop: string, func: (...args: any[]) => any)

image.png

public InitRun(): number

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

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

可以看到 这里的实现和trackInternal的实现几乎一样

image.png

ObserveV2

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

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

  1. ObserveV2.SYMBOL_REF 存储的key为属性名,value 为依赖这个属性的组件的id组成的集合
  2. 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

针对不同的数据结构进行了深度的检测,并且在数据发生变化的时候调用fireChagne

// 造成数组长度改变的方法有这些
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_ 组件id的集合

computedPropIdsChange_计算属性id的集合

monitorIdsChanged_监听函数id的集合

也就是说不管是依赖这个状态属性的计算属性、组件、监听函数都会被调用

ArkUI中组件对应的id是唯一的,并且渲染引擎里默认我们创建的组件数量不会超过1亿

并且在首次这三个集合的总长度为0时开启微任务执行updateDirty函数。为什么总长度为0时开启,是因为监听函数、计算属性有可能修改其他状态变量

image.png

从ArkUIInspector看组件id

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

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

image.png

private updateDirty(): void

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

image.png

private updateDirty2(): void

先处理计算属性,然后处理监听函数,最后处理组件。并且处理他们三个的时候调用对应的方法

image.png

private updateDirtyComputedProps(computed: Array): void

首先更新计算属性,因为计算属性可能会修改正常状态变量的值。最终会调用到ComputedV2的fireChange方法

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

MonitorV2

constructor(target: object, pathsString: string, func: (m: IMonitor) => void) 

MonitorV2的构造方法里将pathsString进行了按照空格的拆分,因为@Monitor传入的参数会被按空格拼接。所以这里拆分出来后的paths对应@Monitor中的各个参数

然后使用各个参数path创建了MonitorValueV2

image.png

MonitorValueV2

image.png

InitRun(): MonitorV2

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

这里比较重要的是analysisProp方法,我们可以看到方法里是从最外层开始递归往内层找属性在不在对象上,直到找到最内层的值。然后返回这个值

所以我们给@Monitor传参的时候也要切记一层层点过去,不能中断

image.png

总结

有@Computed装饰属性的对象

owningObject[ComputedV2.COMPUTED_PREFIX+owningObjectName] 是一个对象
每一个
属性名 是被@Computed装饰的属性名
属性值 是被@Computed装饰的属性值

owningObject[ObserveV2.COMPUTED_REFS] 是一个对象
每一个
属性名 是被@Computed装饰的属性名
属性值 是ComputedV2对象实例,这个对象实例持有 @Computed装饰的属性的 原有owningObject,名字,值

有@Monitor装饰属性的对象

owningObject[ComputedV2.WATCH_PREFIX+owningObjectName] 是一个对象
每一个
属性名 是被@Monitor装饰的属性名
属性值 是被@Monitor装饰的属性值

owningObject[ObserveV2.MONITOR_REFS] 是一个对象
每一个
属性名 是被@Monitor装饰的属性名
属性值 是onitorV2M对象实例,这个对象实例持有 @Monitor装饰的属性的 原有owningObject,名字,值

所有V2版本的状态装饰器装饰的对象

owningObject[ObserveV2.SYMBOL_REFS] 是一个类
每一个
属性名 是被 装饰器 装饰的属性名
属性值 是 使用这些属性的组件的id 组成的Set

owningObject[ObserveV2.ID_REFS] 是一个对象
每一个
属性名 组件id
属性值 是 这个组件使用的状态变量的名字 组成的Set

ObserveV2.getInstance().id2targets_ 是一个对象
每一个
属性名 组件id
属性值 是 这个组件使用的状态变量所属类的对象实例 组成的Set

图示

image.png

参考资料

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