鸿蒙状态管理方案

593 阅读6分钟

@State装饰器

  • 用于组件内的状态同步,私有属性,只能从组件内部访问,声明时必须制定类型并且本地初始化,也可以使用命名参数机制从父组件完成初始化。
  • 装饰的变量生命周期与其所属自定义组件的生命周期相同
  • 装饰的变量与子组件中的@Prop装饰变量之间建立单向数据同步,与@Link、@ObjectLink装饰变量之间建立双向数据同步
  • 允许装饰的变量类型:Object、class、string、number、boolean、enum类型,以及这些类型的数组。
  • 类型必须被指定
  • 不支持any;不支持简单类型和复杂类型的联合类型(如 Length、ResourceStr、ResourceColor);不允许使用undefined和null;建议不要装饰Date类型,应用可能会产生异常行为。
  • 装饰的数据类型为boolean、string、number类型,可以观察到数值的变化,装饰class或者Object类型时,可以观察到自身的赋值的变化,和Object.keys(observedObject)返回的属性赋值变化

@Prop装饰器

  • 装饰的变量可以和父组件建立单向的同步关系

  • 4.0版本只支持string、number、boolean、enum类型,NEXT版本支持的类型同@State装饰器相同

  • 不能在@Entry装饰的自定义组件中使用

  • 被装饰变量允许本地初始化

  • 私有属性,只能从组件内部访问

  • @Prop装饰的数据更新依赖其所属自定义组件的重新渲染,所以在应用进入后台后,@Prop无法刷新,推荐使用@Link代替。

  • 如果本地有初始化,则从父组件初始化是可选的,没有的话,则必须从父组件初始化

  • 更新机制:

    image.png

@Link装饰器

  • 装饰的变量与其父组件中对应的数据源建立双向数据绑定。

  • 不能在@Entry装饰的自定义组件中使用

  • 禁止本地初始化

  • 父组件中@State, @StorageLink和@Link 和子组件@Link可以建立双向数据同步,反之亦然

  • 允许装饰的变量类型同@State相同

  • 必须从父组件初始化

  • 私有,只能在所属组件内访问

  • 更新流程

    附官网截图:

    image.png

    个人理解:父组件在初始化子组件@Link装饰的变量后,当前子组件的指向@Link变量的指针this实际为父组件,即在子组件内部访问this.linkA时,实际访问的是super.LinkA,因此当父组件的LinkA状态变更时,可以引起子组件对应组件的重新渲染,子组件LinkA更新引起父组件变量更新同理。实际父子组件访问的是同一变量

@Provide装饰器和@Consume装饰器

  • 与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景

  • @Provide和@Consume可以通过相同的变量名或者相同的变量别名绑定,变量类型必须相同。

    // 通过相同的变量名绑定
    @Provide a: number = 0;
    @Consume a: number;
    
    // 通过相同的变量别名绑定
    @Provide('a') b: number = 0;
    @Consume('a') c: number;
    
  • 支持的数据类型同@State相同

  • 被@Consume装饰得变量禁止本地初始化

  • 私有,只能在所属组件内访问

  • 更新机制

    image.png

@Observed装饰器和@ObjectLink装饰器

  • 用于在涉及嵌套对象或数组的场景中进行双向数据同步

  • 被@Observed装饰的类,可以被观察到属性的变化

  • 子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰

  • 单独使用@Observed是没有任何作用的,需要搭配@ObjectLink或者@Prop使用

  • 使用@Observed装饰class会改变class原始的原型链,@Observed和其他类装饰器装饰同一个class可能会带来问题

  • @ObjectLink装饰器不能在@Entry装饰的自定义组件中使用

  • @ObjectLink装饰的变量不允许设置初始值,必须从父组件初始化

  • @ObjectLink装饰的变量,必须为被@Observed装饰的class实例,必须指定类型。@ObjectLink的属性是可以改变的,装饰变量是只读的,不能被改变

    // 允许@ObjectLink装饰的数据属性赋值
    this.objLink.a= ...
    // 不允许@ObjectLink装饰的数据自身赋值
    this.objLink= ...
    
  • @Observed装饰的类,如果其属性为非简单类型,比如class、Object或者数组,也需要被@Observed装饰,否则将观察不到其属性的变化。

    // ClassB被@Observed装饰,其成员变量的赋值的变化是可以被观察到的
    // 但对于ClassA,没有被@Observed装饰,其属性的修改不能被观察到。
    class ClassA {
      public c: number;
    
      constructor(c: number) {
        this.c = c;
      }
     }
    
    @Observed
    class ClassB {
      public a: ClassA;
      public b: number;
    
      constructor(a: ClassA, b: number) {
        this.a = a;
        this.b = b;
      }
    }
    
  • 更新机制

    image.png

LocalStorage

页面级UI状态存储,通常用于UIAbility内、页面间的状态共享。

  • LocalStorage中的所有属性都是可变的
  • LocalStorage创建后,命名属性的类型不可更改。后续调用Set时必须使用相同类型的值。
  • @LocalStorageProp(key)是和LocalStorage中key对应的属性建立单向数据同步
  • @LocalStorageLink(key)是和LocalStorage中key对应的属性建立双向数据同步
  • @LocalStorageProp 和 @LocalStorageLink 允许装饰的数据类型同 @State 相同
  • @LocalStorageProp 和 @LocalStorageLink 装饰的变量初始值必须指定如果LocalStorage实例中不存在属性,则作为初始化默认值,并存入LocalStorage中。
  • @LocalStorageProp 和 @LocalStorageLink,不允许从父节点初始化,可以初始化子节点,私有属性不支持组件外部访问
  • 应用程序决定LocalStorage对象的生命周期。当应用释放最后一个指向LocalStorage的引用时,比如销毁最后一个自定义组件,LocalStorage将被JS Engine垃圾回收。
1. 实现页面级的UI状态共享
  • 被@Entry装饰的@Component,可以被分配一个LocalStorage实例,此组件的所有子组件实例将自动获得对该LocalStorage实例的访问权限

  • 被@Component装饰的组件最多可以访问一个LocalStorage实例和AppStorage,未被@Entry装饰的组件不可被独立分配LocalStorage实例,只能接受父组件通过@Entry传递来的LocalStorage实例。一个LocalStorage实例在组件树上可以被分配给多个组件。

      // 代码示例
      let storage = new LocalStorage({ countStorage: 1 });
    
      @Component
      struct Child {
        // 子组件实例的名字
        label: string = 'no name';
        // 和LocalStorage中“countStorage”的双向绑定数据
        @LocalStorageLink('countStorage') playCountLink: number = 0;
    
        build() {
          Row() {
            Text(this.label)
              .width(50).height(60).fontSize(12)
            Text(`playCountLink ${this.playCountLink}: inc by 1`)
              .onClick(() => {
                this.playCountLink += 1;
              })
              .width(200).height(60).fontSize(12)
          }.width(300).height(60)
        }
      }
    
      @Entry(storage)
      @Component
      struct Parent {
        @LocalStorageLink('countStorage') playCount: number = 0;
    
        build() {
          Column() {
            Row() {
              Text('Parent')
                .width(50).height(60).fontSize(12)
              Text(`playCount ${this.playCount} dec by 1`)
                .onClick(() => {
                  this.playCount -= 1;
                })
                .width(250).height(60).fontSize(12)
            }.width(300).height(60)
    
            Row() {
              Text('LocalStorage')
                .width(50).height(60).fontSize(12)
              Text(`countStorage ${this.playCount} incr by 1`)
                .onClick(() => {
                  storage.set<number>('countStorage', 1 + storage.get<number>('countStorage'));
                })
                .width(250).height(60).fontSize(12)
            }.width(300).height(60)
    
            Child({ label: 'ChildA' })
            Child({ label: 'ChildB' })
    
            Text(`playCount in LocalStorage for debug ${storage.get<number>('countStorage')}`)
              .width(300).height(60).fontSize(12)
          }
        }
      }
    
    
2. 实现UIAbility实例内共享
  • LocalStorage是页面级存储,getShared接口仅能获取当前Stage通过windowStage.loadContent传入的LocalStorage实例,否则返回undefined。

  • 在所属UIAbility中创建LocalStorage实例,并调用windowStage.loadContent方法传入进行共享

     // EntryAbility.ts
    import UIAbility from '@ohos.app.ability.UIAbility';
    import window from '@ohos.window';
    
    export default class EntryAbility extends UIAbility {
    para:Record<string, number> = { 'PropA': 47 };
    storage: LocalStorage = new LocalStorage(this.para);
    
    onWindowStageCreate(windowStage: window.WindowStage) {
      windowStage.loadContent('pages/Index', this.storage);
    }
    }
    
  • 在UI页面通过GetShared接口获取在通过loadContent共享的LocalStorage实例

AppStorage

特殊的单例LocalStorage对象,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储;

  • AppStorage是应用级的全局状态共享

  • 持久化数据PersistentStorage环境变量Environment都是通过和AppStorage中转,才可以和UI交互

  • 如果希望这些数据在UI中使用,需要用到@StorageProp@StorageLink

  • @StorageProp 和 @StorageLink使用同 @LocalStorageProp 和 @LocalStorageLink.

  • 限制条件

    • 在AppStorage中创建属性后,调用PersistentStorage.persistProp()接口时,会使用在AppStorage中已经存在的值,并覆盖PersistentStorage中的同名属性,所以建议要使用相反的调用顺序,反例可见在PersistentStorage之前访问AppStorage中的属性
    • 如果在AppStorage中已经创建属性后,再调用Environment.envProp()创建同名的属性,会调用失败。因为AppStorage已经有同名属性,Environment环境变量不会再写入AppStorage中,所以建议AppStorage中属性不要使用Environment预置环境变量名。
    • 状态装饰器装饰的变量,改变会引起UI的渲染更新,如果改变的变量不是用于UI更新,只是用于消息传递,推荐使用 emitter方式。例子可见不建议借助@StorageLink的双向同步机制实现事件通知

PersistentStorage

持久化存储UI状态,通常和AppStorage配合使用,选择AppStorage存储的数据写入磁盘,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同;

  • PersistentStorage和AppStorage中的属性建立双向同步。应用开发通常通过AppStorage访问PersistentStorage,另外还有一些接口可以用于管理持久化属性,但是业务逻辑始终是通过AppStorage获取和设置属性的。

  • 4.0版本,不支持undefined 和 null,如果缓存的数据是对象结构,需要对数据进行序列化JSON.stringfiy()

  • NEXT版本,从API version 12开始,PersistentStorage支持null、undefined;复杂数据类型可以直接写入;

  • 限制条件

    • 避免持久化大型数据集
    • 避免持久化经常变化的变量
    • PersistentStorage的持久化变量最好是小于2kb的数据,不要大量的数据持久化,因为PersistentStorage写入磁盘的操作是同步的,大量的数据本地化读写会同步在UI线程中执行,影响UI渲染性能。
    • PersistentStorage只能在UI页面内使用,否则将无法持久化数据。
  • 支持的接口

    • 设置需要持久化的属性
       // 将AppStorage中key对应的属性持久化到文件中。该接口的调用通常在访问AppStorage之前。
       static PersistProp<T>(key: string, defaultValue: T): void
       // 示例
       // defaultValue,在PersistentStorage和AppStorage未查询到时,则使用默认值初始化初始化它。不允许为undefined和null。
       PersistentStorage.PersistProp('highScore', '0');
    
    • 删除已经持久化的属性
    // 将key对应的属性从PersistentStorage删除,后续AppStorage的操作,对PersistentStorage不会再有影响。
    static DeleteProp(key: string): void
     // 示例
    PersistentStorage.DeleteProp('highScore');
    
    • 一次性持久化多个数据,适合在应用启动的时候初始化。
    static PersistProps(properties: {key: string, defaultValue: any;}[]): void
    // 示例
    PersistentStorage.PersistProps([{ key: 'highScore', defaultValue: '0' }, { key: 'wightScore', defaultValue: '1' }]);
    
    • 返回所有持久化属性的key的数组
    let keys: Array<string> = PersistentStorage.Keys();
    

Environment

  • 应用程序运行的设备的环境参数(如多语言,暗黑模式),环境参数会同步到AppStorage中,可以和AppStorage搭配使用。
  • Environment是ArkUI框架在应用程序启动时创建的单例对象。它为AppStorage提供了一系列描述应用程序运行状态的属性。Environment的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型。
  • Environment内置参数
    • accessibilityEnabled: 获取无障碍屏幕读取是否启用。
    • colorMode: ColorMode.LIGHT: 浅色,ColorMode.DARK: 深色
    • fontScale: 字体大小比例,范围: [0.85, 1.45]
    • fontWeightScale: 字体粗细程度,范围: [0.6, 1.6]
    • layoutDirection: 布局方向类型:包括LayoutDirection.LTR: 从左到右,LayoutDirection.RTL: 从右到左
    • languageCode: 当前系统语言值,取值必须为小写字母, 例如zh
  • 使用场景
    // 将设备的语言code存入AppStorage,默认值为en
    Environment.EnvProp('languageCode', 'en');
    // 可以使用@StorageProp链接到Component中。
    @StorageProp('languageCode') lang : string = 'en';
    
  • 注意:@StorageProp关联的环境参数可以在本地更改,但不能同步回AppStorage中,因为应用对环境变量参数是不可写的,只能在Environment中查询。

@Watch装饰器

  • 用于监听状态变量的变化
  • @Watch在ArkUI框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。当在严格相等为false的情况下,就会触发@Watch的回调。
  • 建议@State、@Prop、@Link等装饰器在@Watch装饰器之前。
  • 在第一次初始化的时候,@Watch装饰的方法不会被调用,即认为初始化不是状态变量的改变
  • 属性值更新函数会延迟组件的重新渲染(具体请见上面的行为表现),因此,回调函数应仅执行快速运算;
  • 不建议在@Watch函数中调用async await,因为@Watch设计的用途是为了快速的计算,异步行为可能会导致重新渲染速度的性能问题。
  • 代码示例
    @Component
    struct TotalView {
      @Prop @Watch('onCountUpdated') count: number;
      @State total: number = 0;
      // @Watch 回调
      onCountUpdated(propName: string): void {
        this.total += this.count;
      }
    
      build() {
        Text(`Total: ${this.total}`)
      }
    }
    
    @Entry
    @Component
    struct CountModifier {
      @State count: number = 0;
    
      build() {
        Column() {
          Button('add to basket')
            .onClick(() => {
              this.count++
            })
          TotalView({ count: this.count })
        }
      }
    }
    

$$双向绑定

  • 用于内置组件的双向同步
  • 【$$】绑定的变量变化时,会触发UI的同步刷新
  • 【$$】支持基础类型变量,以及@State、@Link和@Prop装饰的变量
  • 4.0版本仅支持Refresh组件的refreshing参数, NEXT版本支持更多参数或者属性