鸿蒙开发(二)页面组件级UI状态存储

500 阅读9分钟

状态管理概述

在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。

自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。下图展示了State和View(UI)之间的关系。

zh-cn_image_0000001562352677

  • View(UI):UI渲染,指将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面。
  • State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。

页面级变量的状态管理

@State装饰器 组件内状态

@State装饰的变量,与声明式范式中的其他被装饰变量一样,是私有的,只能从组件内部访问,在声明时必须指定其类型和本地初始化。初始化也可选择使用命名参数机制从父组件完成初始化。

@State装饰的变量拥有以下特点:

  • @State装饰的变量与子组件中的@Prop装饰变量之间建立单向数据同步,与@Link、@ObjectLink装饰变量之间建立双向数据同步。
  • @State装饰的变量生命周期与其所属自定义组件的生命周期相同。

框架行为

  • 当状态变量被改变时,查询依赖该状态变量的组件;
  • 执行依赖该状态变量的组件的更新方法,组件更新渲染;
  • 和该状态变量不相关的组件或者UI描述不会发生重新渲染,从而实现页面渲染的按需更新。

@Prop装饰器 父子单向同步

@Prop装饰的变量和父组件建立单向的同步关系:

  • @Prop变量允许在本地修改,但修改后的变化不会同步回父组件。
  • 当数据源更改时,@Prop装饰的变量都会更新,并且会覆盖本地所有更改。因此,数值的同步是父组件到子组件(所属组件),子组件数值的变化不会同步到父组件。

注:

  • @Prop装饰变量时会进行深拷贝,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。例如PixelMap等通过NAPI提供的复杂类型,由于有部分实现在Native侧,因此无法在ArkTS侧通过深拷贝获得完整的数据。
  • @Prop装饰器不能在@Entry装饰的自定义组件中使用。
  • @Prop装饰的数据更新依赖其所属自定义组件的重新渲染,所以在应用进入后台后,@Prop无法刷新,推荐使用@Link代替

框架行为

要理解@Prop变量值初始化和更新机制,有必要了解父组件和拥有@Prop变量的子组件初始渲染和更新流程。

  • 初始渲染:

    1. 执行父组件的build()函数将创建子组件的新实例,将数据源传递给子组件;
    2. 初始化子组件@Prop装饰的变量。
  • 更新:

    1. 子组件@Prop更新时,更新仅停留在当前子组件,不会同步回父组件;
    2. 当父组件的数据源更新时,子组件的@Prop装饰的变量将被来自父组件的数据源重置,所有@Prop装饰的本地的修改将被父组件的更新覆盖。

@Link装饰器 父子双向同步

子组件中变量被@Link修饰器修饰,会与父组件对应的变量建立起双向绑定关系。@Link装饰的变量与其父组件中的数据源共享相同的值。

框架行为

@Link装饰的变量和其所属的自定义组件共享生命周期。

为了了解@Link变量初始化和更新机制,有必要先了解父组件和拥有@Link变量的子组件的关系,初始渲染和双向更新的流程(以父组件为@State为例)。

  • 初始渲染:执行父组件的build()函数后将创建子组件的新实例。初始化过程如下:
  1. 必须指定父组件中的@State变量,用于初始化子组件的@Link变量。子组件的@Link变量值与其父组件的数据源变量保持同步(双向数据同步)。
  2. 父组件的@State状态变量包装类通过构造函数传给子组件,子组件的@Link包装类拿到父组件的@State的状态变量后,将当前@Link包装类this指针注册给父组件的@State变量。
  • @Link的数据源的更新:即父组件中状态变量更新,引起相关子组件的@Link的更新。处理步骤:
  1. 通过初始渲染的步骤可知,子组件@Link包装类把当前this指针注册给父组件。父组件@State变量变更后,会遍历更新所有依赖它的系统组件(elementid)和状态变量(比如@Link包装类)。
  2. 通知@Link包装类更新后,子组件中所有依赖@Link状态变量的系统组件(elementId)都会被通知更新。以此实现父组件对子组件的状态数据同步。
  • @Link的更新:当子组件中@Link更新后,处理步骤如下(以父组件为@State为例):
  1. @Link更新后,调用父组件的@State包装类的set方法,将更新后的数值同步回父组件。
  2. 孩子组件@Link和父组件@State分别遍历依赖的系统组件,进行对应的UI的更新。以此实现子组件@Link同步回父组件@State。

@Provide装饰器和@Consume装饰器:与后代组件双向同步

@Provide/@Consume装饰的状态变量有以下特性:

  • @Provide装饰的状态变量自动对其所有后代组件可用,即该变量被“provide”给他的后代组件。由此可见,@Provide的方便之处在于,开发者不需要多次在组件之间传递变量。
  • 后代通过使用@Consume去获取@Provide提供的变量,建立在@Provide和@Consume之间的双向数据同步,与@State/@Link不同的是,前者可以在多层级的父子组件之间传递。
  • @Provide和@Consume可以通过相同的变量名或者相同的变量别名绑定,建议类型相同,否则会发生类型隐式转换,从而导致应用行为异常。

@Provide和@Consume通过相同的变量名或者相同的变量别名绑定时,@Provide装饰的变量和@Consume装饰的变量是一对多的关系。不允许在同一个自定义组件内,包括其子组件中声明多个同名或者同别名的@Provide装饰的变量,@Provide的属性名或别名需要唯一且确定,如果声明多个同名或者同别名的@Provide装饰的变量,会发生运行时报错。

框架行为

  • 初始渲染:

    1. @Provide装饰的变量会以map的形式,传递给当前@Provide所属组件的所有子组件;
    2. 子组件中如果使用@Consume变量,则会在map中查找是否有该变量名/alias(别名)对应的@Provide的变量,如果查找不到,框架会抛出JS ERROR;
    3. 在初始化@Consume变量时,和@State/@Link的流程类似,@Consume变量会在map中查找到对应的@Provide变量进行保存,并把自己注册给@Provide。
  • 当@Provide装饰的数据变化时:

    1. 通过初始渲染的步骤可知,子组件@Consume已把自己注册给父组件。父组件@Provide变量变更后,会遍历更新所有依赖它的系统组件(elementid)和状态变量(@Consume);
    2. 通知@Consume更新后,子组件所有依赖@Consume的系统组件(elementId)都会被通知更新。以此实现@Provide对@Consume状态数据同步。
  • 当@Consume装饰的数据变化时:

    通过初始渲染的步骤可知,子组件@Consume持有@Provide的实例。在@Consume更新后调用@Provide的更新方法,将更新的数值同步回@Provide,以此实现@Consume向@Provide的同步更新。

Provide_Consume_framework_behavior

@Observed装饰器和@ObjectLink装饰器:嵌套类对象属性变化

上述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。

@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:

  • 被@Observed装饰的类,可以被观察到属性的变化;
  • 子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰。
  • @Observed用于嵌套类场景中,观察对象类属性变化,要配合自定义组件使用,如果要做数据双/单向同步,需要搭配@ObjectLink或者@Prop使用。

框架行为

  • 初始渲染:
  1. @Observed装饰的class的实例会被不透明的代理对象包装,代理了class上的属性的setter和getter方法
  2. 子组件中@ObjectLink装饰的从父组件初始化,接收被@Observed装饰的class的实例,@ObjectLink的包装类会将自己注册给@Observed class。
  • 属性更新:当@Observed装饰的class属性改变时,会走到代理的setter和getter,然后遍历依赖它的@ObjectLink包装类,通知数据更新。