状态管理(V2)

213 阅读7分钟

1、@Local组件内部状态

  • @Local装饰的变量只能在组件内部初始化
  • @Local只能装饰变量本身
    • 对于简单类型string、number、boolean可以观察到赋值变化
    • 对于对象,监听到整体赋值,无法监听属性变化(如需要监听属性,请使用ObservedV2 + @Trace
    • 对于Array,监听到整体赋值与数组元素项的变化
    • 其他类型:Date、Map、Set、Array可以观察到整体赋值和API调用引起的变化

V1@State对比

+ `@State`允许从外部初始化,也可以本地初始化,`@Local`仅允许本地初始化
+ `@State`对于对象类型可以观察到整体赋值与第一层属性变化,`@Local`对于对象类型仅能观察到赋值(深层监听依赖`ObservedV2 + @Trace`)
// 1、基本数据类型均可观察变化
@Local age: number = 18;
// 2、类对象类型观察到整体赋值,无法观察到对象属性变化
@Local person: Person = new Person('小玉', 18);
// 3、Array类型观察到整体赋值,也可以观察到元素项变化
@Local arr: string[] = ['a'];
// 4、Map、Date、Set等内置对象观察到整体赋值和由于API调用引起的变化,如:Map.set() Set.add() Date.setFullYear()
@Local mapping: Map<string, string> = new Map();
// 5、Array嵌套复杂类型时,可观察到Array的整体赋值,元素项的监听同上,如元素项是类对象则对应第2条规则
@Local list: Person[] = [new Person('', 0)];

2、@Param组件外部输入 @Once初始化同步一次

  • @Param可以外部初始化,也支持本地初始化,当同时存在本地初始值与外部传入值时,会优先使用外部传入值进行初始化
    • @Param @Require搭配使用表示必须从外部传入初始化
    • @Param @Once搭配使用表示仅从外部初始化一次、不接受后续同步变化的能力,并且此时允许组件内部修改变量
  • @Param装饰的变量不允许组件内部修改,只能等数据源(必须为状态变量才能同步)变化时同步给子组件
  • 对于复杂类型,@Param接受数据源的引用(但此时组件内部对类对象属性的修改会同步给数据源)

V1@Prop对比

+ `@Prop`对于复杂类型会深拷贝,性能差,`@Param`接受数据源的引用
// 1、可以外部初始化,也可以本地初始化,同时存在则使用外部
@Param name: string = '';
// 2、与@Require搭配使用表示必须由外部初始化
@Param @Require age: number;
// 3、与@Once搭配使用表示仅从外部初始化一次、不接受后续同步变化的能力,此时变量可以在组件内部修改
@Param @Once msg: string = '';

3、@Event规范组件输出

  • @Event主要配合@Param实现数据的父子同步,由于@Param在组件本地无法修改,需要父组件通过自定义方法(修改状态变量的方法)传入到子组件,子组件内部调用触发,父组件再同步回子组件实现双向同步
// Child.ets
// @Param标志着组件的输入
@Param msg: string = '';
// @Event标志着组件的输出,可以通过该方法影响父组件
@Event changeMsg: () => void = () => {};

// Father
@Local msg: string = 'Hello World';
changeMsg() {
    this.msg = 'Hello ArkTS'
}
build() {
    // changeMsg通过自定义方法
    Child({ msg: this.msg, changeMsg: () => { this.changeMsg() } })
}

4、@ObservedV2类对象的深度监听 @Trace与按需监听

  • @ObservedV2装饰器仅能装饰class@Trace装饰器仅能装饰类的成员属性,需要配合使用,单独使用不起作用
  • 只有被@Trace装饰的类成员属性变更时,才会通知依赖该属性的UI更新
  • 多层嵌套类场景时,仅被直接使用的属性需要被@Trace装饰且该属性所属类需要被@ObservedV2装饰

V1@Observed & @ObjectLink对比

+ `V1 @Observed`搭配`@Track`使用,但是需要使用`@ObjectLink`接收`@Observed`装饰的类的实例,这需要额外创建组件
@ObservedV2
class Info {
    // 只需要被直接使用的属性@Trace装饰,所属类被@ObservedV2装饰
    @Trace num: number = 1
}

class Person {
    name: string;
    age: number;
    info: Info;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
        this.info = new Info()
    }
}

@ComponentV2
struct StateV2 {
    @Local person: Person = new Person('', 18);

    build() {
        Column({ space: 10 }) {
            Button(`${this.person.info.num}`)
            .onClick(() => {
                this.person.info.num += 1;
            })
        }
    }
}

// V1 针对多层嵌套类场景,@ObjectLink接收被@Observed需要单独创建组件
@Component
struct StateV1 {
    @State person: Person = new Person('', 18);

    build() {
        Column({ space: 10 }) {
            // 为了接收@Observed装饰的类的实例,需要额外创建一个组件
            ChildComponent({ info: this.person.info })
        }
    }
}

@Component
struct ChildComponent {
    @ObjectLink info: Info;
    build() {
        Button(`${this.info.num}`)
            .onClick(() => {
                this.info.num += 1;
            })
    }
}

5、@Monitor监听状态变量修改

  • 仅能监听状态变量,无法监听普通变量
  • 可一次性监听多个属性
  • 具有深度监听的能力,可监听嵌套类、多维数组、对象数组中指定项的变化

V1@Watch对比

  • @Watch仅能监听单个数据,@Monitor可以多数据监听
  • @Watch不能获取变化前的值,@Monitor可以获取变化前的值
  • @Monitor可以整体监听,也可以监听被@Trace装饰的类成员属性,@Watch只能整体监听
  • @Monitor支持监听多层嵌套场景,@Watch针对类对象状态变量的整体赋值与第一层属性变化
  • @Monitor可以在@ObservedV2装饰的类中使用,@Watch不能
@ObservedV2
class Info {
    @Trace weight: number = 90;
}

@ObservedV2
class Person {
    // age被@Trace装饰,能够监听变化
    @Trace age: number = 18;
    // name未被@Trace装饰,不能够监听变化
    name: string = '';
    info: Info = new Info();

    // 1、@Monitor可以在@ObservedV2装饰的类中使用
    @Monitor('age')
    ageChange(monitor: IMonitor) {}

    // 2、@Monitor可以监听深层属性的变化,该深层属性需要被@Trace装饰
    @Monitor('info.weight')
    weightChange(monitor: IMonitor) {}
}

@ComponentV2
export struct StateV2 {
    @Local num: number = 1;
    @Local person: Person = new Person('', 18);
    @Local arr: Person[][] = [[new Person('', 0), new Person('', 1)], []]
    // 支持多属性监听、深度监听,类对象属性需要被@Trace装饰
    @Monitor('num', 'person.info.weight', 'arr.0', 'arr.0.1.info.weight')
    ageChange(monitor: IMonitor) {
        monitor.dirty.forEach((path: string) => {
            hilog.info(0x0123, '', `属性 ${path}:变化前 = ${monitor.value(path)?.before};变化后 = ${monitor.value(path)?.now}`)
        })
    }

    build() {}
}

6、@Computed计算属性

  • 方法装饰器,仅能装饰getter方法,惰性计算,仅依赖属性变化时重新求值
  • 可以在@ObservedV2装饰的类中使用@Computed
// 1、在组件中使用
@ComponentV2
export struct StateV2 {
    @Local num: number = 1;
    @Local person: Person = new Person('张三', 18);

    @Computed
    get valName() {
        return `${this.person.name} => ${this.num}`
    }
}

// 2、在@ObservedV2装饰的类中使用@Computed
@ObservedV2
class Person {
    @Trace name: string;
    @Trace age: number;
    info: Info;
    constructor(name: string, age: number) {}

    @Computed
    get allMsg() {
        return `${this.name}:年龄${this.age},体重${this.info.weight}`
    }
}

7、AppStorageV2应用全局UI状态管理

  • connect(type, key?, defaultCreator) 创建或获取储存的数据,key未指定时,则使用typename作为key
  • remove(keyOrType) 删除指定key的储存数据,如果参数为type则使用typename作为key
  • keys() 返回所有AppStorageV2中的key
@ObservedV2
class SafeArea {
  @Trace top: number;
  @Trace bottom: number;

  constructor(top: number, bottom: number) {
    this.top = top;
    this.bottom = bottom;
  }
}

export default class EntryAbility extends UIAbility {
    onCreate() {
        // AppStorageV2是应用级别的,可以在全局任意地方使用
        AppStorageV2.connect(SafeArea, () => new SafeArea(top.topRect.height, bottom.bottomRect.height))
    }
}

@ComponentV2
export struct StateV2 {
    // connect仅一个参数type时,需确保有值否则程序奔溃crash
    @Local area: SafeArea = AppStorageV2.connect(SafeArea)!;

    build() {
        Colume() {
            Text(`${this.area.top}`)
                // 更新数据
                .onClick(() => {
                    // 直接修改通过 AppStorageV2.connect 获取的对象属性即可。修改后的数据会自动同步到 AppStorageV2 中
                    this.area.top += 1;
                })
        }
    }
}

V2 中不存在也不需要页面级的状态管理 LocalStorage,使用@ObservedV2/@Trace实现相同效果

  • 声明@ObservedV2装饰的类
  • 声明被@Trace的属性作为页面间共享的可观察的数据
  • 状态管理V2将观察能力增强到数据本身,数据本身就是可观察的,更改数据会触发相应的视图的更新,因此只需要对同一个类实例进行修改会触发所有引用该实例的UI刷新
// Constant.ets
// 写法1,单例
@ObservedV2
export class BaseInfo {
  @Trace num: number = 1;
  static singleton_: BaseInfo;
  static instance() {
    if (!BaseInfo.singleton_) {
      BaseInfo.singleton_ = new BaseInfo();
    }
    return BaseInfo.singleton_;
  }
}
// 写法2,直接导出实例
@ObservedV2
export class UserInfo {
  @Trace age: number = 1
}
export const userInfo: UserInfo = new UserInfo();
// 此时数据在AB页面之间共享
// PageA.ets
import { BaseInfo, userInfo, UserInfo } from './Constant.ets'
@ComponentV2
export struct PageA {
    baseInfo: BaseInfo = BaseInfo.instance();
    @Local userInfo: UserInfo = userInfo;

    build() {
        Column() {
            Button('click').onClick(() => {
                this.baseInfo.num += 1;
            })
            Button(`${this.userInfo.age}`).onClick(() => {
                // 页面A的修改会同步给B
                this.userInfo.age += 1;
            })
        }
    }
}
// PageB.ets
import { BaseInfo, userInfo, UserInfo } from './Constant.ets'
export struct PageB {
    baseInfo: BaseInfo = BaseInfo.instance();
    @Local userInfo: UserInfo = userInfo;

    build() {
        Column() {
            Button('click').onClick(() => {
                this.baseInfo.num += 1;
            })
            Button(`${this.userInfo.age}`).onClick(() => {
                // 页面B的修改会同步给A
                this.userInfo.age += 1;
            })
        }
    }
}

8、@Provider 和 @Consumer 跨组件层级双向同步

@Provider(aliasName?: string) varName: varType = initValue; @Consumer(aliasName?: string) varName: varType = defaultValue;

  • aliasName别名,缺省时使用属性名varName
  • 必须本地初始化,区别:V1(@Provide)允许从父组件初始化
  • 可以装饰箭头函数,区别:V1(@Provide)不支持
  • 装饰复杂类型时依赖@ObservedV2@Trace
@ObservedV2
class Person {
    @Trace age: number = 18;
}
// 父组件
@Provider() appName: string = '鸿蒙App';
@Provider('aliasName') varName: number = 1;
// 装饰箭头函数
@Provider() onDrag: (x: number, y: number) => void = (x: number, y: number) => {
    hilog.info(0x0123, '', `${x} => ${y}`)
}
// 复杂类型
@Provider('person') p: Person = new Person();

// 子孙组件
@Consumer() appName: string = ''; // 缺省别名则使用属性名向上查找最近的同名属性
@Consumer('aliasName') localName: string = '';
@Consumer() onDrag: (x: number, y: number) => void = () => {};
@Consumer() person: Person | undefined;

9、PersistenceV2 持久化存储状态

  • PersistenceV2是在应用UI启动时会被创建的单例,会将最新数据储存在设备磁盘上(持久化)

  • 持久化的数据必须是class对象,不能是容器(如Array、Set、Map),不能是内置的构造对象(如Date、Number)

  • 单个key支持数据大小约8k,过大会导致持久化失败

  • 持久化存储@ObservedV2 & @Trace的数据改变会触发自动持久化,其他数据需要手动触发持久化

  • connect(type, keyOrDefaultCreater?, defaultCreator?) 创建或获取储存的数据

    • 未指定key则使用type.name作为key,此时第二个参数指默认构造器
  • remove(keyOrType) 删除指定key的储存数据

  • keys() 返回所有PersistenceV2中的key

  • save(keyOrType) 手动持久化数据

  • notifyOnError(callback) 响应序列化或反序列化失败的回调

    • 将数据存入磁盘时,需要对数据进行序列化;当某个key序列化失败时,错误是不可预知的;可调用该接口捕获异常
@ObservedV2
class Result {
    code: number = 0;
    @Trace msg: string = '成功';
}
// 创建或获取储存的数据
@Local main: Result = PersistenceV2.connect(Result, 'Result', () => new Result())!;

msgChange() {
    // @ObservedV2 & @Trace装饰的数据会自动持久化
    this.main.msg = '失败';
}
codeChange() {
    this.main.code = -1;
    // 未被@Trace装饰的成员如果需要进行持久化保存需要手动触发 save
    PersistenceV2.save('Result');
}

10、!!语法糖 双向绑定

  • @Event 方法名需要申明为 '$' + @Param属性名
Child({ value: this.value!! }) // 语法糖,相当于 Child({ value: this.value, $value: (val: string) => { this.value = val; } })

// Child
@Param value: string = '';
@Event $value: (val: string) => void = () => {};

handleChange() {
    this.$value('xxx');
}

11、makeObserved 将非观察数据变为可观察数据

  • makeObserved可以在@Trace无法标记的情况下使用
    • class的定义在三方包中
    • interface或者JSON.parse返回的匿名对象
  • 仅支持非空的对象类型传参
  • makeObserved不支持传入被@ObservedV2、@Observed装饰的类的实例以及已经被makeObserved封装过的代理数据。为了防止双重代理,makeObserved发现入参为上述情况时则直接返回,不做处理
  • makeObserved可以使用在 V1 @Component中,但不能搭配装饰器使用
class NavConfig {
    navName: string
    constructor(name: string) {
        this.navName = name
    }
}
@ComponentV2
struct Index {
    // 将非观察数据JSON.parse()返回的匿名对象变为可观察数据,不需要装饰器修饰
    navConfig: NavConfig = UIUtils.makeObserved(JSON.parse(JSON.stringify(new NavConfig('Home'))));
    // makeObserved可以和V2的装饰器一起使用,使对象具有深度观察能力
    @Local navConfig: NavConfig = UIUtils.makeObserved(new NavConfig('Home'));

    handleChange() {
        // 会触发UI更新
        this.navConfig.navName = 'List';
    }
}

// makeObserved可以使用在@Component中使用,使数据具有观察能力
@Component
struct Index {
    // 数据已具有观察能力,可以触发UI刷新
    message: Info = UIUtils.makeObserved(new Info(20));
}

12、UIUtils.getTarget 获取状态管理框架代理前的原始对象

class Person {
    age = 1;
}
const person = new Person();

@ComponentV2
struct Index {
    private p: Person = UIUtils.makeObserved(person);

    aboutToAppear() {
        // 1、getTarget仅支持对象类型传参
        UIUtils.getTarget(1); // Argument of type 'number' is not assignable to parameter of type 'object'. <ArkTSCheck>

        const temp = UIUtils.getTarget(this.p); // 获取代理前的原始对象
        temp  === person; // true
        // 2、更改getTarget获取的原始对象中的内容不会被观察到变化,也不会触发UI刷新
        temp.age += 1;
    }
}