状态管理
我们构建的页面多为静态界面。如果希望构建一个动态的、有交互的界面,就需要引入“状态”的概念。
上面的示例中,用户与应用程序的交互触发了文本状态变更,状态变更引起了UI渲染,UI从“Hello World”变更为“Hello ArkUI”。
在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。
自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。 下图展示了State和View(UI)之间的关系。
-
View(UI):UI渲染,指将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面。
-
State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。
基本概念
-
状态变量:被状态装饰器装饰的变量,状态变量值的改变会引起UI的渲染更新。示例:@State num: number = 1,其中,@State是状态装饰器,num是状态变量。
-
常规变量:没有被状态装饰器装饰的变量,通常应用于辅助计算。它的改变永远不会引起UI的刷新。以下示例中increaseBy变量为常规变量。
-
数据源/同步源:状态变量的原始来源,可以同步给不同的状态数据。通常意义为父组件传给子组件的数据。以下示例中数据源为count: 1。
-
命名参数机制:父组件通过指定参数传递给子组件的状态变量,为父子传递同步参数的主要手段。示例:CompA({ aProp: this.aProp })。
-
从父组件初始化:父组件使用命名参数机制,将指定参数传递给子组件。子组件初始化的默认值在有父组件传值的情况下,会被覆盖。示例:
@Component struct MyComponent { @State count: number = 0; private increaseBy: number = 1; build() { } } @Component struct Parent { build() { Column() { // 从父组件初始化,覆盖本地定义的默认值 MyComponent({ count: 1, increaseBy: 2 }) } } }
-
初始化子组件:父组件中状态变量可以传递给子组件,初始化子组件对应的状态变量。示例同上。
-
本地初始化:在变量声明的时候赋值,作为变量的默认值。示例:@State count: number = 0。
状态管理(V1稳定版)
开发者可以使用当前比较完整的状态管理V1版本进行应用开发。
装饰器总览
ArkUI状态管理V1提供了多种装饰器,通过使用这些装饰器,状态变量不仅可以观察在组件内的改变,还可以在不同组件层级间传递,比如父子组件、跨组件层级,也可以观察全局范围内的变化。根据状态变量的影响范围,将所有的装饰器可以大致分为:
-
管理组件拥有状态的装饰器:组件级别的状态管理,可以观察组件内变化,和不同组件层级的变化,但需要唯一观察同一个组件树上,即同一个页面内。
-
管理应用拥有状态的装饰器:应用级别的状态管理,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。
从数据的传递形式和同步类型层面看,装饰器也可分为:
-
只读的单向传递;
-
可变更的双向传递。
图示如下,具体装饰器的介绍,可详见管理组件拥有的状态和管理应用拥有的状态。开发者可以灵活地利用这些能力来实现数据和UI的联动。
上图中,Components部分的装饰器为组件级别的状态管理,Application部分为应用的状态管理。开发者可以通过@StorageLink/@LocalStorageLink实现应用和组件状态的双向同步,通过@StorageProp/@LocalStorageProp实现应用和组件状态的单向同步。
管理组件拥有的状态,即图中Components级别的状态管理:
-
@State:@State装饰的变量拥有其所属组件的状态,可以作为其子组件单向和双向同步的数据源。当其数值改变时,会引起相关组件的渲染刷新。
-
@Prop:@Prop装饰的变量可以和父组件建立单向同步关系,@Prop装饰的变量是可变的,但修改不会同步回父组件。
-
@Link:@Link装饰的变量可以和父组件建立双向同步关系,子组件中@Link装饰变量的修改会同步给父组件中建立双向数据绑定的数据源,父组件的更新也会同步给@Link装饰的变量。
-
@Provide/@Consume:@Provide/@Consume装饰的变量用于跨组件层级(多层组件)同步状态变量,可以不需要通过参数命名机制传递,通过alias(别名)或者属性名绑定。
-
@Observed:@Observed装饰class,需要观察多层嵌套场景的class需要被@Observed装饰。单独使用@Observed没有任何作用,需要和@ObjectLink、@Prop联用。
-
@ObjectLink:@ObjectLink装饰的变量接收@Observed装饰的class的实例,应用于观察多层嵌套场景,和父组件的数据源构建双向同步。
说明
仅@Observed/@ObjectLink可以观察嵌套场景,其他的状态变量仅能观察第一层,详情见各个装饰器章节的“观察变化和行为表现”小节。
管理应用拥有的状态,即图中Application级别的状态管理:
-
AppStorage是应用程序中的一个特殊的单例LocalStorage对象,是应用级的数据库,和进程绑定,通过@StorageProp和@StorageLink装饰器可以和组件联动。
-
AppStorage是应用状态的“中枢”,将需要与组件(UI)交互的数据存入AppStorage,比如持久化数据PersistentStorage和环境变量Environment。UI再通过AppStorage提供的装饰器或者API接口,访问这些数据。
-
框架还提供了LocalStorage,AppStorage是LocalStorage特殊的单例。LocalStorage是应用程序声明的应用状态的内存“数据库”,通常用于页面级的状态共享,通过@LocalStorageProp和@LocalStorageLink装饰器可以和UI联动。
其他状态管理V1功能
@Watch用于监听状态变量的变化。
$$运算符:给内置组件提供TS变量的引用,使得TS变量和内置组件的内部状态保持同步。
管理组件拥有的状态
@State装饰器:组件内状态
@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就可以触发其直接绑定UI组件的刷新。当状态改变时,UI会发生对应的渲染改变。
在状态变量相关装饰器中,@State是最基础的,使变量拥有状态属性的装饰器,它也是大部分状态变量的数据源。
概述
@State装饰的变量,与声明式范式中的其他被装饰变量一样,是私有的,只能从组件内部访问,在声明时必须指定其类型和本地初始化。初始化也可选择使用命名参数机制从父组件完成初始化。
@State装饰的变量拥有以下特点:
-
@State装饰的变量与子组件中的@Prop装饰变量之间建立单向数据同步,与@Link、@ObjectLink装饰变量之间建立双向数据同步。
-
@State装饰的变量生命周期与其所属自定义组件的生命周期相同。
装饰器使用规则说明
@State变量装饰器 | 说明 |
---|---|
装饰器参数 | 无 |
同步类型 | 不与父组件中任何类型的变量同步。 |
允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。 支持Date类型。 支持undefined和null类型。 支持ArkUI框架定义的联合类型Length、ResourceStr、ResourceColor类型。 类型必须被指定。 支持类型的场景请参考观察变化。 不支持any。 API11及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见@State支持联合类型实例。 注意 当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@State a : string | undefined = undefined是推荐的,不推荐@State a: string = undefined。 |
被装饰变量的初始值 | 必须本地初始化。 |
变量的传递/访问规则说明
传递/访问 | 说明 |
---|---|
从父组件初始化 | 可选,从父组件初始化或者本地初始化。如果从父组件初始化将会覆盖本地初始化。 支持父组件中常规变量(常规变量对@State赋值,只是数值的初始化,常规变量的变化不会触发UI刷新,只有状态变量才能触发UI刷新)、@State、@Link、@Prop、@Provide、@Consume、@ObjectLink、@StorageLink、@StorageProp、@LocalStorageLink和@LocalStorageProp装饰的变量,初始化子组件的@State。 |
用于初始化子组件 | @State装饰的变量支持初始化子组件的常规变量、@State、@Link、@Prop、@Provide。 |
是否支持组件外访问 | 不支持,只能在组件内访问。 |
观察变化和行为表现
并不是状态变量的所有更改都会引起UI的刷新,只有可以被框架观察到的修改才会引起UI刷新。本小节将介绍什么样的修改才能被观察到,以及观察到变化后,框架的是怎么引起UI刷新的,即框架的行为表现是什么。
观察变化
-
当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
// for simple type @State count: number = 0; // value changing can be observed this.count = 1;
-
当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,即Object.keys(observedObject)返回的所有属性。例子如下。
声明ClassA和Model类。
class ClassA { public value: string; constructor(value: string) { this.value = value; } } class Model { public value: string; public name: ClassA; constructor(value: string, a: ClassA) { this.value = value; this.name = a; } }
@State装饰的类型是Model
// class类型 @State title: Model = new Model('Hello', new ClassA('World'));
对@State装饰变量的赋值。
// class类型赋值 this.title = new Model('Hi', new ClassA('ArkUI'));
对@State装饰变量的属性赋值。
// class属性的赋值 this.title.value = 'Hi';
嵌套属性的赋值观察不到。
// 嵌套的属性赋值观察不到 this.title.name.value = 'ArkUI';
-
当装饰的对象是array时,可以观察到数组本身的赋值和添加、删除、更新数组的变化。例子如下。
声明Model类。
class Model { public value: number; constructor(value: number) { this.value = value; } }
@State装饰的对象为Model类型数组时。
// 数组类型 @State title: Model[] = [new Model(11), new Model(1)];
数组自身的赋值可以观察到。
// 数组赋值 this.title = [new Model(2)];
数组项的赋值可以观察到。
// 数组项赋值 this.title[0] = new Model(2);
删除数组项可以观察到。
// 数组项更改 this.title.pop();
新增数组项可以观察到。
// 数组项更改 this.title.push(new Model(12));
数组项中属性的赋值观察不到。
// 嵌套的属性赋值观察不到 this.title[0].value = 6;
-
当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。
-
当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量。
-
当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量。
框架行为
-
当状态变量被改变时,查询依赖该状态变量的组件;
-
执行依赖该状态变量的组件的更新方法,组件更新渲染;
-
和该状态变量不相关的组件或者UI描述不会发生重新渲染,从而实现页面渲染的按需更新。
@Prop装饰器:父子单向同步
@Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。
概述
@Prop装饰的变量和父组件建立单向的同步关系:
-
@Prop变量允许在本地修改,但修改后的变化不会同步回父组件。
-
当数据源更改时,@Prop装饰的变量都会更新,并且会覆盖本地所有更改。因此,数值的同步是父组件到子组件(所属组件),子组件数值的变化不会同步到父组件。
限制条件
-
@Prop装饰变量时会进行深拷贝,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。例如PixelMap等通过NAPI提供的复杂类型,由于有部分实现在Native侧,因此无法在ArkTS侧通过深拷贝获得完整的数据。
-
@Prop装饰器不能在@Entry装饰的自定义组件中使用。
装饰器使用规则说明
@Prop变量装饰器 | 说明 |
---|---|
装饰器参数 | 无 |
同步类型 | 单向同步:对父组件状态变量值的修改,将同步给子组件@Prop装饰的变量,子组件@Prop变量的修改不会同步到父组件的状态变量上。嵌套类型的场景请参考观察变化。 |
允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。 不支持any,支持undefined和null。 支持Date类型。 API11及以上支持Map、Set类型。 支持ArkUI框架定义的联合类型Length、ResourceStr、ResourceColor类型。 必须指定类型。 @Prop和数据源类型需要相同,有以下三种情况: - @Prop装饰的变量和@State以及其他装饰器同步时双方的类型必须相同,示例请参考父组件@State到子组件@Prop简单数据类型同步。 - @Prop装饰的变量和@State以及其他装饰器装饰的数组的项同步时 ,@Prop的类型需要和@State装饰的数组的数组项相同,比如@Prop : T和@State : Array<T>,示例请参考父组件@State数组中的项到子组件@Prop简单数据类型同步。 - 当父组件状态变量为Object或者class时,@Prop装饰的变量和父组件状态变量的属性类型相同,示例请参考从父组件中的@State类对象属性到@Prop简单类型的同步。 支持类型的场景请参考观察变化。 API11及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见Prop支持联合类型实例。 注意 当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@Prop a : string | undefined = undefined是推荐的,不推荐@Prop a: string = undefined。 |
嵌套传递层数 | 在组件复用场景,建议@Prop深度嵌套数据不要超过5层,嵌套太多会导致深拷贝占用的空间过大以及GarbageCollection(垃圾回收),引起性能问题,此时更建议使用@ObjectLink。 |
被装饰变量的初始值 | 允许本地初始化。如果在API 11中和@Require结合使用,则必须父组件构造传参。 |
变量的传递/访问规则说明
传递/访问 | 说明 |
---|---|
从父组件初始化 | 如果本地有初始化,则是可选的。没有的话,则必选,支持父组件中的常规变量(常规变量对@Prop赋值,只是数值的初始化,常规变量的变化不会触发UI刷新。只有状态变量才能触发UI刷新)、@State、@Link、@Prop、@Provide、@Consume、@ObjectLink、@StorageLink、@StorageProp、@LocalStorageLink和@LocalStorageProp去初始化子组件中的@Prop变量。 |
用于初始化子组件 | @Prop支持去初始化子组件中的常规变量、@State、@Link、@Prop、@Provide。 |
是否支持组件外访问 | @Prop装饰的变量是私有的,只能在组件内访问。 |
图1 初始化规则图示
观察变化和行为表现
观察变化
@Prop装饰的数据可以观察到以下变化。
-
当装饰的类型是允许的类型,即Object、class、string、number、boolean、enum类型都可以观察到赋值的变化。
// 简单类型 @Prop count: number; // 赋值的变化可以被观察到 this.count = 1; // 复杂类型 @Prop title: Model; // 可以观察到赋值的变化 this.title = new Model('Hi');
-
当装饰的类型是Object或者class复杂类型时,可以观察到第一层的属性的变化,属性即Object.keys(observedObject)返回的所有属性;
class ClassA { public value: string; constructor(value: string) { this.value = value; } } class Model { public value: string; public a: ClassA; constructor(value: string, a: ClassA) { this.value = value; this.a = a; } } @Prop title: Model; // 可以观察到第一层的变化 this.title.value = 'Hi' // 观察不到第二层的变化 this.title.a.value = 'ArkUi'
对于嵌套场景,如果class是被@Observed装饰的,可以观察到class属性的变化,示例请参考@Prop嵌套场景。
- 当装饰的类型是数组的时候,可以观察到数组本身的赋值和数组项的添加、删除和更新。
// @State装饰的对象为数组时
@Prop title: string[]
// 数组自身的赋值可以观察到
this.title = ['1']
// 数组项的赋值可以观察到
this.title[0] = '2'
// 删除数组项可以观察到
this.title.pop()
// 新增数组项可以观察到
this.title.push('3')
对于@State和@Prop的同步场景:
-
使用父组件中@State变量的值初始化子组件中的@Prop变量。当@State变量变化时,该变量值也会同步更新至@Prop变量。
-
@Prop装饰的变量的修改不会影响其数据源@State装饰变量的值。
-
除了@State,数据源也可以用@Link或@Prop装饰,对@Prop的同步机制是相同的。
-
数据源和@Prop变量的类型需要相同,@Prop允许简单类型和class类型。
-
当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。
@Component struct DateComponent { @Prop selectedDate: Date = new Date(''); build() { Column() { Button('child update the new date') .margin(10) .onClick(() => { this.selectedDate = new Date('2023-09-09') }) Button(`child increase the year by 1`).onClick(() => { this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1) }) DatePicker({ start: new Date('1970-1-1'), end: new Date('2100-1-1'), selected: this.selectedDate }) } } } @Entry @Component struct ParentComponent { @State parentSelectedDate: Date = new Date('2021-08-08'); build() { Column() { Button('parent update the new date') .margin(10) .onClick(() => { this.parentSelectedDate = new Date('2023-07-07') }) Button('parent increase the day by 1') .margin(10) .onClick(() => { this.parentSelectedDate.setDate(this.parentSelectedDate.getDate() + 1) }) DatePicker({ start: new Date('1970-1-1'), end: new Date('2100-1-1'), selected: this.parentSelectedDate }) DateComponent({ selectedDate: this.parentSelectedDate }) } } }
-
当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量。
-
当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量。
框架行为
要理解@Prop变量值初始化和更新机制,有必要了解父组件和拥有@Prop变量的子组件初始渲染和更新流程。
-
初始渲染:
- 执行父组件的build()函数将创建子组件的新实例,将数据源传递给子组件;
- 初始化子组件@Prop装饰的变量。
-
更新:
- 子组件@Prop更新时,更新仅停留在当前子组件,不会同步回父组件;
- 当父组件的数据源更新时,子组件的@Prop装饰的变量将被来自父组件的数据源重置,所有@Prop装饰的本地的修改将被父组件的更新覆盖。
说明
@Prop装饰的数据更新依赖其所属自定义组件的重新渲染,所以在应用进入后台后,@Prop无法刷新,推荐使用@Link代替。
@Link装饰器:父子双向同步
子组件中被@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。
概述
@Link装饰的变量与其父组件中的数据源共享相同的值。
限制条件
- @Link装饰器不能在@Entry装饰的自定义组件中使用。
装饰器使用规则说明
@Link变量装饰器 | 说明 |
---|---|
装饰器参数 | 无 |
同步类型 | 双向同步。 父组件中@State, @StorageLink和@Link 和子组件@Link可以建立双向数据同步,反之亦然。 |
允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。 支持Date类型。 API11及以上支持Map、Set类型。 支持ArkUI框架定义的联合类型Length、ResourceStr、ResourceColor类型。 类型必须被指定,且和双向绑定状态变量的类型相同。 支持类型的场景请参考观察变化。 不支持any。 API11及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见Link支持联合类型实例。 注意 当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@Link a : string | undefined。 |
被装饰变量的初始值 | 无,禁止本地初始化。 |
变量的传递/访问规则说明
传递/访问 | 说明 |
---|---|
从父组件初始化和更新 | 必选。与父组件@State, @StorageLink和@Link 建立双向绑定。允许父组件中@State、@Link、@Prop、@Provide、@Consume、@ObjectLink、@StorageLink、@StorageProp、@LocalStorageLink和@LocalStorageProp装饰变量初始化子组件@Link。 从API version 9开始,@Link子组件从父组件初始化@State的语法为Comp({ aLink: this.aState })。同样Comp({aLink: $aState})也支持。 |
用于初始化子组件 | 允许,可用于初始化常规变量、@State、@Link、@Prop、@Provide。 |
是否支持组件外访问 | 私有,只能在所属组件内访问。 |
观察变化和行为表现
观察变化
-
当装饰的数据类型为boolean、string、number类型时,可以同步观察到数值的变化,示例请参考简单类型和类对象类型的@Link。
-
当装饰的数据类型为class或者Object时,可以观察到赋值和属性赋值的变化,即Object.keys(observedObject)返回的所有属性,示例请参考简单类型和类对象类型的@Link。
-
当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化,示例请参考数组类型的@Link。
-
当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。
@Component struct DateComponent { @Link selectedDate: Date; build() { Column() { Button(`child increase the year by 1`).onClick(() => { this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1) }) Button('child update the new date') .margin(10) .onClick(() => { this.selectedDate = new Date('2023-09-09') }) DatePicker({ start: new Date('1970-1-1'), end: new Date('2100-1-1'), selected: this.selectedDate }) } } } @Entry @Component struct ParentComponent { @State parentSelectedDate: Date = new Date('2021-08-08'); build() { Column() { Button('parent increase the month by 1') .margin(10) .onClick(() => { this.parentSelectedDate.setMonth(this.parentSelectedDate.getMonth() + 1) }) Button('parent update the new date') .margin(10) .onClick(() => { this.parentSelectedDate = new Date('2023-07-07') }) DatePicker({ start: new Date('1970-1-1'), end: new Date('2100-1-1'), selected: this.parentSelectedDate }) DateComponent({ selectedDate:this.parentSelectedDate }) } } }
-
当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量。
-
当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量。
框架行为
@Link装饰的变量和其所属的自定义组件共享生命周期。
为了了解@Link变量初始化和更新机制,有必要先了解父组件和拥有@Link变量的子组件的关系,初始渲染和双向更新的流程(以父组件为@State为例)。
-
初始渲染:执行父组件的build()函数后将创建子组件的新实例。初始化过程如下:
- 必须指定父组件中的@State变量,用于初始化子组件的@Link变量。子组件的@Link变量值与其父组件的数据源变量保持同步(双向数据同步)。
- 父组件的@State状态变量包装类通过构造函数传给子组件,子组件的@Link包装类拿到父组件的@State的状态变量后,将当前@Link包装类this指针注册给父组件的@State变量。
-
@Link的数据源的更新:即父组件中状态变量更新,引起相关子组件的@Link的更新。处理步骤:
- 通过初始渲染的步骤可知,子组件@Link包装类把当前this指针注册给父组件。父组件@State变量变更后,会遍历更新所有依赖它的系统组件(elementid)和状态变量(比如@Link包装类)。
- 通知@Link包装类更新后,子组件中所有依赖@Link状态变量的系统组件(elementId)都会被通知更新。以此实现父组件对子组件的状态数据同步。
-
@Link的更新:当子组件中@Link更新后,处理步骤如下(以父组件为@State为例):
- @Link更新后,调用父组件的@State包装类的set方法,将更新后的数值同步回父组件。
- 子组件@Link和父组件@State分别遍历依赖的系统组件,进行对应的UI的更新。以此实现子组件@Link同步回父组件@State。
@Provide装饰器和@Consume装饰器:与后代组件双向同步
@Provide和@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递,@Provide和@Consume摆脱参数传递机制的束缚,实现跨层级传递。
其中@Provide装饰的变量是在祖先组件中,可以理解为被“提供”给后代的状态变量。@Consume装饰的变量是在后代组件中,去“消费(绑定)”祖先组件提供的变量。
概述
@Provide/@Consume装饰的状态变量有以下特性:
-
@Provide装饰的状态变量自动对其所有后代组件可用,即该变量被“provide”给他的后代组件。由此可见,@Provide的方便之处在于,开发者不需要多次在组件之间传递变量。
-
后代通过使用@Consume去获取@Provide提供的变量,建立在@Provide和@Consume之间的双向数据同步,与@State/@Link不同的是,前者可以在多层级的父子组件之间传递。
-
@Provide和@Consume可以通过相同的变量名或者相同的变量别名绑定,建议类型相同,否则会发生类型隐式转换,从而导致应用行为异常。
// 通过相同的变量名绑定
@Provide a: number = 0;
@Consume a: number;
// 通过相同的变量别名绑定
@Provide('a') b: number = 0;
@Consume('a') c: number;
@Provide和@Consume通过相同的变量名或者相同的变量别名绑定时,@Provide装饰的变量和@Consume装饰的变量是一对多的关系。不允许在同一个自定义组件内,包括其子组件中声明多个同名或者同别名的@Provide装饰的变量,@Provide的属性名或别名需要唯一且确定,如果声明多个同名或者同别名的@Provide装饰的变量,会发生运行时报错。
装饰器说明
@State的规则同样适用于@Provide,差异为@Provide还作为多层后代的同步源。
@Provide变量装饰器 | 说明 |
---|---|
装饰器参数 | 别名:常量字符串,可选。 如果指定了别名,则通过别名来绑定变量;如果未指定别名,则通过变量名绑定变量。 |
同步类型 | 双向同步。 从@Provide变量到所有@Consume变量以及相反的方向的数据同步。双向同步的操作与@State和@Link的组合相同。 |
允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。 支持Date类型。 API11及以上支持Map、Set类型。 支持ArkUI框架定义的联合类型Length、ResourceStr、ResourceColor类型。 必须指定类型。 @Provide变量的@Consume变量的类型必须相同。 支持类型的场景请参考观察变化。 不支持any。 API11及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见@Provide_and_Consume支持联合类型实例。 注意 当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@Provide a : string | undefined = undefined是推荐的,不推荐@Provide a: string = undefined。 |
被装饰变量的初始值 | 必须指定。 |
支持allowOverride参数 | 允许重写,只要声明了allowOverride,则别名和属性名都可以被Override。示例见@Provide支持allowOverride参数。 |
@Consume变量装饰器 | 说明 |
---|---|
装饰器参数 | 别名:常量字符串,可选。 如果提供了别名,则必须有@Provide的变量和其有相同的别名才可以匹配成功;否则,则需要变量名相同才能匹配成功。 |
同步类型 | 双向:从@Provide变量(具体请参见@Provide)到所有@Consume变量,以及相反的方向。双向同步操作与@State和@Link的组合相同。 |
允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。 支持Date类型。 支持ArkUI框架定义的联合类型Length、ResourceStr、ResourceColor类型。必须指定类型。 @Provide变量和@Consume变量的类型必须相同。 @Consume装饰的变量,在其父组件或者祖先组件上,必须有对应的属性和别名的@Provide装饰的变量。 支持类型的场景请参考观察变化。 不支持any。 API11及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见@Provide_and_Consume支持联合类型实例。 注意 当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@Consume a : string | undefined。 |
被装饰变量的初始值 | 无,禁止本地初始化。 |
变量的传递/访问规则说明
@Provide传递/访问 | 说明 |
---|---|
从父组件初始化和更新 | 可选,允许父组件中常规变量(常规变量对@Provide赋值,只是数值的初始化,常规变量的变化不会触发UI刷新,只有状态变量才能触发UI刷新)、@State、@Link、@Prop、@Provide、@Consume、@ObjectLink、@StorageLink、@StorageProp、@LocalStorageLink和@LocalStorageProp装饰的变量装饰变量初始化子组件@Provide。 |
用于初始化子组件 | 允许,可用于初始化@State、@Link、@Prop、@Provide。 |
和父组件同步 | 否。 |
和后代组件同步 | 和@Consume双向同步。 |
是否支持组件外访问 | 私有,仅可以在所属组件内访问。 |
@Consume传递/访问 | 说明 |
---|---|
从父组件初始化和更新 | 禁止。通过相同的变量名和alias(别名)从@Provide初始化。 |
用于初始化子组件 | 允许,可用于初始化@State、@Link、@Prop、@Provide。 |
和祖先组件同步 | 和@Provide双向同步。 |
是否支持组件外访问 | 私有,仅可以在所属组件内访问 |
观察变化和行为表现
观察变化
-
当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
-
当装饰的数据类型为class或者Object的时候,可以观察到赋值和属性赋值的变化(属性为Object.keys(observedObject)返回的所有属性)。
-
当装饰的对象是array的时候,可以观察到数组的添加、删除、更新数组单元。
-
当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。
@Component struct CompD { @Consume selectedDate: Date; build() { Column() { Button(`child increase the day by 1`) .onClick(() => { this.selectedDate.setDate(this.selectedDate.getDate() + 1) }) Button('child update the new date') .margin(10) .onClick(() => { this.selectedDate = new Date('2023-09-09') }) DatePicker({ start: new Date('1970-1-1'), end: new Date('2100-1-1'), selected: this.selectedDate }) } } } @Entry @Component struct CompA { @Provide selectedDate: Date = new Date('2021-08-08') build() { Column() { Button('parent increase the day by 1') .margin(10) .onClick(() => { this.selectedDate.setDate(this.selectedDate.getDate() + 1) }) Button('parent update the new date') .margin(10) .onClick(() => { this.selectedDate = new Date('2023-07-07') }) DatePicker({ start: new Date('1970-1-1'), end: new Date('2100-1-1'), selected: this.selectedDate }) CompD() } } }
-
当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量。
-
当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量。
框架行为
-
初始渲染:
- @Provide装饰的变量会以map的形式,传递给当前@Provide所属组件的所有子组件;
- 子组件中如果使用@Consume变量,则会在map中查找是否有该变量名/alias(别名)对应的@Provide的变量,如果查找不到,框架会抛出JS ERROR;
- 在初始化@Consume变量时,和@State/@Link的流程类似,@Consume变量会在map中查找到对应的@Provide变量进行保存,并把自己注册给@Provide。
-
当@Provide装饰的数据变化时:
- 通过初始渲染的步骤可知,子组件@Consume已把自己注册给父组件。父组件@Provide变量变更后,会遍历更新所有依赖它的系统组件(elementid)和状态变量(@Consume);
- 通知@Consume更新后,子组件所有依赖@Consume的系统组件(elementId)都会被通知更新。以此实现@Provide对@Consume状态数据同步。
-
当@Consume装饰的数据变化时:
通过初始渲染的步骤可知,子组件@Consume持有@Provide的实例。在@Consume更新后调用@Provide的更新方法,将更新的数值同步回@Provide,以此实现@Consume向@Provide的同步更新。
@Observed装饰器和@ObjectLink装饰器:嵌套类对象属性变化
上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。
概述
@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:
-
被@Observed装饰的类,可以被观察到属性的变化;
-
子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰。
-
@Observed用于嵌套类场景中,观察对象类属性变化,要配合自定义组件使用(示例详见嵌套对象),如果要做数据双/单向同步,需要搭配@ObjectLink或者@Prop使用(示例详见@Prop与@ObjectLink的差异)。
限制条件
-
使用@Observed装饰class会改变class原始的原型链,@Observed和其他类装饰器装饰同一个class可能会带来问题。
-
@ObjectLink装饰器不能在@Entry装饰的自定义组件中使用。
装饰器说明
@Observed类装饰器 | 说明 |
---|---|
装饰器参数 | 无 |
类装饰器 | 装饰class。需要放在class的定义前,使用new创建类对象。 |
@ObjectLink变量装饰器 | 说明 |
---|---|
装饰器参数 | 无 |
允许装饰的变量类型 | 必须为被@Observed装饰的class实例,必须指定类型。 不支持简单类型,可以使用@Prop。 支持继承Date、Array的class实例,API11及以上支持继承Map、Set的class实例。示例见观察变化。 API11及以上支持@Observed装饰类和undefined或null组成的联合类型,比如ClassA | ClassB, ClassA | undefined 或者 ClassA | null, 示例见@ObjectLink支持联合类型。 @ObjectLink的属性是可以改变的,但是变量的分配是不允许的,也就是说这个装饰器装饰变量是只读的,不能被改变。 |
被装饰变量的初始值 | 不允许。 |
@ObjectLink装饰的数据为可读示例。
// 允许@ObjectLink装饰的数据属性赋值
this.objLink.a= ...
// 不允许@ObjectLink装饰的数据自身赋值
this.objLink= ...
说明
@ObjectLink装饰的变量不能被赋值,如果要使用赋值操作,请使用@Prop。
@Prop装饰的变量和数据源的关系是是单向同步,@Prop装饰的变量在本地拷贝了数据源,所以它允许本地更改,如果父组件中的数据源有更新,@Prop装饰的变量本地的修改将被覆盖;
@ObjectLink装饰的变量和数据源的关系是双向同步,@ObjectLink装饰的变量相当于指向数据源的指针。禁止对@ObjectLink装饰的变量赋值,如果一旦发生@ObjectLink装饰的变量的赋值,则同步链将被打断。因为@ObjectLink装饰的变量通过数据源(Object)引用来初始化。对于实现双向数据同步的@ObjectLink,赋值相当于更新父组件中的数组项或者class的属性,TypeScript/JavaScript不能实现,会发生运行时报错。
变量的传递/访问规则说明
@ObjectLink传递/访问 | 说明 |
---|---|
从父组件初始化 | 必须指定。 初始化@ObjectLink装饰的变量必须同时满足以下场景: - 类型必须是@Observed装饰的class。 - 初始化的数值需要是数组项,或者class的属性。 - 同步源的class或者数组必须是@State,@Link,@Provide,@Consume或者@ObjectLink装饰的数据。 |
与源对象同步 | 双向。 |
可以初始化子组件 | 允许,可用于初始化常规变量、@State、@Link、@Prop、@Provide |
观察变化和行为表现
观察变化
@Observed装饰的类,如果其属性为非简单类型,比如class、Object或者数组,也需要被@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;
}
}
以上示例中,ClassB被@Observed装饰,其成员变量的赋值的变化是可以被观察到的,但对于ClassA,没有被@Observed装饰,其属性的修改不能被观察到。
@ObjectLink b: ClassB
// 赋值变化可以被观察到
this.b.a = new ClassA(5)
this.b.b = 5
// ClassA没有被@Observed装饰,其属性的变化观察不到
this.b.a.c = 5
@ObjectLink:@ObjectLink只能接收被@Observed装饰class的实例,推荐设计单独的自定义组件来渲染每一个数组或对象。此时,对象数组或嵌套对象(属性是对象的对象称为嵌套对象)需要两个自定义组件,一个自定义组件呈现外部数组/对象,另一个自定义组件呈现嵌套在数组/对象内的类对象。可以观察到:
-
其属性的数值的变化,其中属性是指Object.keys(observedObject)返回的所有属性,示例请参考嵌套对象。
-
如果数据源是数组,则可以观察到数组item的替换,如果数据源是class,可观察到class的属性的变化,示例请参考对象数组。
继承Date的class时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。
@Observed
class DateClass extends Date {
constructor(args: number | string) {
super(args)
}
}
@Observed
class ClassB {
public a: DateClass;
constructor(a: DateClass) {
this.a = a;
}
}
@Component
struct ViewA {
label: string = 'date';
@ObjectLink a: DateClass;
build() {
Column() {
Button(`child increase the day by 1`)
.onClick(() => {
this.a.setDate(this.a.getDate() + 1);
})
DatePicker({
start: new Date('1970-1-1'),
end: new Date('2100-1-1'),
selected: this.a
})
}
}
}
@Entry
@Component
struct ViewB {
@State b: ClassB = new ClassB(new DateClass('2023-1-1'));
build() {
Column() {
ViewA({ label: 'date', a: this.b.a })
Button(`parent update the new date`)
.onClick(() => {
this.b.a = new DateClass('2023-07-07');
})
Button(`ViewB: this.b = new ClassB(new DateClass('2023-08-20'))`)
.onClick(() => {
this.b = new ClassB(new DateClass('2023-08-20'));
})
}
}
}
继承Map的class时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见继承Map类。
继承Set的class时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见继承Set类。
框架行为
-
初始渲染:
- @Observed装饰的class的实例会被不透明的代理对象包装,代理了class上的属性的setter和getter方法
- 子组件中@ObjectLink装饰的从父组件初始化,接收被@Observed装饰的class的实例,@ObjectLink的包装类会将自己注册给@Observed class。
-
属性更新:当@Observed装饰的class属性改变时,会走到代理的setter和getter,然后遍历依赖它的@ObjectLink包装类,通知数据更新。
管理应用拥有的状态
LocalStorage:页面级UI状态存储
LocalStorage是页面级的UI状态存储,通过@Entry装饰器接收的参数可以在页面内共享同一个LocalStorage实例。LocalStorage支持UIAbility实例内多个页面间状态共享。
概述
LocalStorage是ArkTS为构建页面级别状态变量提供存储的内存内的“数据库”。
-
应用程序可以创建多个LocalStorage实例,LocalStorage实例可以在页面内共享,也可以通过GetShared接口,实现跨页面、UIAbility实例内共享。
-
组件树的根节点,即被@Entry装饰的@Component,可以被分配一个LocalStorage实例,此组件的所有子组件实例将自动获得对该LocalStorage实例的访问权限。
-
被@Component装饰的组件最多可以访问一个LocalStorage实例和AppStorage,未被@Entry装饰的组件不可被独立分配LocalStorage实例,只能接受父组件通过@Entry传递来的LocalStorage实例。一个LocalStorage实例在组件树上可以被分配给多个组件。
-
LocalStorage中的所有属性都是可变的。
应用程序决定LocalStorage对象的生命周期。当应用释放最后一个指向LocalStorage的引用时,比如销毁最后一个自定义组件,LocalStorage将被JS Engine垃圾回收。
LocalStorage根据与@Component装饰的组件的同步类型不同,提供了两个装饰器:
-
@LocalStorageProp:@LocalStorageProp装饰的变量与LocalStorage中给定属性建立单向同步关系。
-
@LocalStorageLink:@LocalStorageLink装饰的变量与LocalStorage中给定属性建立双向同步关系。
限制条件
- LocalStorage创建后,命名属性的类型不可更改。后续调用Set时必须使用相同类型的值。
- LocalStorage是页面级存储,getShared接口仅能获取当前Stage通过windowStage.loadContent传入的LocalStorage实例,否则返回undefined。例子可见将LocalStorage实例从UIAbility共享到一个或多个视图。
@LocalStorageProp
在上文中已经提到,如果要建立LocalStorage和自定义组件的联系,需要使用@LocalStorageProp和@LocalStorageLink装饰器。使用@LocalStorageProp(key)/@LocalStorageLink(key)装饰组件内的变量,key标识了LocalStorage的属性。
当自定义组件初始化的时候,@LocalStorageProp(key)/@LocalStorageLink(key)装饰的变量会通过给定的key,绑定LocalStorage对应的属性,完成初始化。本地初始化是必要的,因为无法保证LocalStorage一定存在给定的key(这取决于应用逻辑是否在组件初始化之前在LocalStorage实例中存入对应的属性)。
@LocalStorageProp(key)是和LocalStorage中key对应的属性建立单向数据同步,ArkUI框架支持修改@LocalStorageProp(key)在本地的值,但是对本地值的修改不会同步回LocalStorage中。相反,如果LocalStorage中key对应的属性值发生改变,例如通过set接口对LocalStorage中的值进行修改,改变会同步给@LocalStorageProp(key),并覆盖掉本地的值。
装饰器使用规则说明
@LocalStorageProp变量装饰器 | 说明 |
---|---|
装饰器参数 | key:常量字符串,必填(字符串需要有引号)。 |
允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。 API12及以上支持Map、Set、Date类型。嵌套类型的场景请参考观察变化和行为表现。 类型必须被指定,建议和LocalStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。 不支持any,API12及以上支持undefined和null类型。 API12及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见LocalStorage支持联合类型。 注意 当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@LocalStorageProp("AA") a: number | null = null是推荐的,不推荐@LocalStorageProp("AA") a: number = null。 |
同步类型 | 单向同步:从LocalStorage的对应属性到组件的状态变量。组件本地的修改是允许的,但是LocalStorage中给定的属性一旦发生变化,将覆盖本地的修改。 |
被装饰变量的初始值 | 必须指定,如果LocalStorage实例中不存在属性,则用该初始值初始化该属性,并存入LocalStorage中。 |
变量的传递/访问规则说明
传递/访问 | 说明 |
---|---|
从父节点初始化和更新 | 禁止,@LocalStorageProp不支持从父节点初始化,只能从LocalStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化。 |
初始化子节点 | 支持,可用于初始化@State、@Link、@Prop、@Provide。 |
是否支持组件外访问 | 否。 |
观察变化和行为表现
观察变化
-
当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
-
当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和对象属性变化(详见从ui内部使用localstorage)。
-
当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
-
当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。详见装饰Date类型变量。
-
当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量。
-
当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量。
框架行为
-
被@LocalStorageProp装饰的变量的值的变化不会同步回LocalStorage里。
-
@LocalStorageProp装饰的变量变化会使当前自定义组件中关联的组件刷新。
-
LocalStorage(key)中值的变化会引发所有被@LocalStorageProp对应key装饰的变量的变化,会覆盖@LocalStorageProp本地的改变。
@LocalStorageLink
如果我们需要将自定义组件的状态变量的更新同步回LocalStorage,就需要用到@LocalStorageLink。
@LocalStorageLink(key)是和LocalStorage中key对应的属性建立双向数据同步:
-
本地修改发生,该修改会被写回LocalStorage中;
-
LocalStorage中的修改发生后,该修改会被同步到所有绑定LocalStorage对应key的属性上,包括单向(@LocalStorageProp和通过prop创建的单向绑定变量)、双向(@LocalStorageLink和通过link创建的双向绑定变量)变量。
装饰器使用规则说明
@LocalStorageLink变量装饰器 | 说明 |
---|---|
装饰器参数 | key:常量字符串,必填(字符串需要有引号)。 |
允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。 API12及以上支持Map、Set、Date类型。嵌套类型的场景请参考观察变化和行为表现。 类型必须被指定,建议和LocalStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。 不支持any,API12及以上支持undefined和null类型。 API12及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见LocalStorage支持联合类型。 注意 当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@LocalStorageLink("AA") a: number | null = null是推荐的,不推荐@LocalStorageLink("AA") a: number = null。 |
同步类型 | 双向同步:从LocalStorage的对应属性到自定义组件,从自定义组件到LocalStorage对应属性。 |
被装饰变量的初始值 | 必须指定,如果LocalStorage实例中不存在属性,则用该初始值初始化该属性,并存入LocalStorage中。 |
变量的传递/访问规则说明
传递/访问 | 说明 |
---|---|
从父节点初始化和更新 | 禁止,@LocalStorageLink不支持从父节点初始化,只能从LocalStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化。 |
初始化子节点 | 支持,可用于初始化@State、@Link、@Prop、@Provide。 |
是否支持组件外访问 | 否。 |
观察变化和行为表现
观察变化
-
当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
-
当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和对象属性变化(详见从ui内部使用localstorage)。
-
当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
-
当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。详见装饰Date类型变量。
-
当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量。
-
当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量。
框架行为
-
当@LocalStorageLink(key)装饰的数值改变被观察到时,修改将被同步回LocalStorage对应属性键值key的属性中。
-
LocalStorage中属性键值key对应的数据一旦改变,属性键值key绑定的所有的数据(包括双向@LocalStorageLink和单向@LocalStorageProp)都将同步修改。
-
当@LocalStorageLink(key)装饰的数据本身是状态变量,它的改变不仅仅会同步回LocalStorage中,还会引起所属的自定义组件的重新渲染。
AppStorage:应用全局的UI状态存储
AppStorage是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。
和AppStorage不同的是,LocalStorage是页面级的,通常应用于页面内的数据共享。而AppStorage是应用级的全局状态共享,还相当于整个应用的“中枢”,持久化数据PersistentStorage和环境变量Environment都是通过AppStorage中转,才可以和UI交互。
概述
AppStorage是在应用启动的时候会被创建的单例。它的目的是为了提供应用状态数据的中心存储,这些状态数据在应用级别都是可访问的。AppStorage将在应用运行过程保留其属性。属性通过唯一的键字符串值访问。
AppStorage可以和UI组件同步,且可以在应用业务逻辑中被访问。
AppStorage支持应用的主线程内多个UIAbility实例间的状态共享。
AppStorage中的属性可以被双向同步,数据可以是存在于本地或远程设备上,并具有不同的功能,比如数据持久化(详见PersistentStorage)。这些数据是通过业务逻辑中实现,与UI解耦,如果希望这些数据在UI中使用,需要用到@StorageProp和@StorageLink。
@StorageProp
在上文中已经提到,如果要建立AppStorage和自定义组件的联系,需要使用@StorageProp和@StorageLink装饰器。使用@StorageProp(key)/@StorageLink(key)装饰组件内的变量,key标识了AppStorage的属性。
当自定义组件初始化的时候,会使用AppStorage中对应key的属性值将@StorageProp(key)/@StorageLink(key)装饰的变量初始化。由于应用逻辑的差异,无法确认是否在组件初始化之前向AppStorage实例中存入了对应的属性,所以AppStorage不一定存在key对应的属性,因此@StorageProp(key)/@StorageLink(key)装饰的变量进行本地初始化是必要的。
@StorageProp(key)是和AppStorage中key对应的属性建立单向数据同步,允许本地改变,但是对于@StorageProp,本地的修改永远不会同步回AppStorage中,相反,如果AppStorage给定key的属性发生改变,改变会被同步给@StorageProp,并覆盖掉本地的修改。
装饰器使用规则说明
@StorageProp变量装饰器 | 说明 |
---|---|
装饰器参数 | key:常量字符串,必填(字符串需要有引号)。 |
允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。 API12及以上支持Map、Set、Date类型。嵌套类型的场景请参考观察变化和行为表现。 类型必须被指定,建议和AppStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。 不支持any,API12及以上支持undefined和null类型。 API12及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见AppStorage支持联合类型。 注意 当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@StorageProp("AA") a: number | null = null是推荐的,不推荐@StorageProp("AA") a: number = null。 |
同步类型 | 单向同步:从AppStorage的对应属性到组件的状态变量。 组件本地的修改是允许的,但是AppStorage中给定的属性一旦发生变化,将覆盖本地的修改。 |
被装饰变量的初始值 | 必须指定,如果AppStorage实例中不存在属性,则用该初始值初始化该属性,并存入AppStorage中。 |
变量的传递/访问规则说明
传递/访问 | 说明 |
---|---|
从父节点初始化和更新 | 禁止,@StorageProp不支持从父节点初始化,只能AppStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化 |
初始化子节点 | 支持,可用于初始化@State、@Link、@Prop、@Provide。 |
是否支持组件外访问 | 否。 |
观察变化和行为表现
观察变化
-
当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
-
当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和对象属性变化(详见从ui内部使用appstorage和localstorage)。
-
当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
-
当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。详见装饰Date类型变量。
-
当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量。
-
当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量。
框架行为
-
当@StorageProp(key)装饰的数值改变被观察到时,修改不会被同步回AppStorage对应key的属性中。
-
当前@StorageProp(key)单向绑定的数据会被修改,即仅限于当前组件的私有成员变量改变,其他绑定该key的数据不会同步改变。
-
当@StorageProp(key)装饰的数据本身是状态变量,它的改变虽然不会同步回AppStorage中,但是会引起所属的自定义组件重新渲染。
-
当AppStorage中key对应的属性发生改变时,会同步给所有@StorageProp(key)装饰的数据,@StorageProp(key)本地的修改将被覆盖。
@StorageLink
@StorageLink(key)是和AppStorage中key对应的属性建立双向数据同步:
-
本地修改发生,该修改会被写回AppStorage中;
-
AppStorage中的修改发生后,该修改会被同步到所有绑定AppStorage对应key的属性上,包括单向(@StorageProp和通过Prop创建的单向绑定变量)、双向(@StorageLink和通过Link创建的双向绑定变量)变量和其他实例(比如PersistentStorage)。
装饰器使用规则说明
@StorageLink变量装饰器 | 说明 |
---|---|
装饰器参数 | key:常量字符串,必填(字符串需要有引号)。 |
允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。 API12及以上支持Map、Set、Date类型。嵌套类型的场景请参考观察变化和行为表现。 类型必须被指定,建议和AppStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。 不支持any,API12及以上支持undefined和null类型。 API12及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见AppStorage支持联合类型。 注意 当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@StorageLink("AA") a: number | null = null是推荐的,不推荐@StorageLink("AA") a: number = null。 |
同步类型 | 双向同步:从AppStorage的对应属性到自定义组件,从自定义组件到AppStorage对应属性。 |
被装饰变量的初始值 | 必须指定,如果AppStorage实例中不存在属性,则用该初始值初始化该属性,并存入AppStorage中。 |
变量的传递/访问规则说明
传递/访问 | 说明 |
---|---|
从父节点初始化和更新 | 禁止。 |
初始化子节点 | 支持,可用于初始化常规变量、@State、@Link、@Prop、@Provide。 |
是否支持组件外访问 | 否。 |
观察变化和行为表现
观察变化
-
当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
-
当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和对象属性变化(详见从ui内部使用appstorage和localstorage)。
-
当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
-
当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。详见装饰Date类型变量。
-
当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量。
-
当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量。
框架行为
-
当@StorageLink(key)装饰的数值改变被观察到时,修改将被同步回AppStorage对应属性键值key的属性中。
-
AppStorage中属性键值key对应的数据一旦改变,属性键值key绑定的所有的数据(包括双向@StorageLink和单向@StorageProp)都将同步修改。
-
当@StorageLink(key)装饰的数据本身是状态变量,它的改变不仅仅会同步回AppStorage中,还会引起所属的自定义组件的重新渲染。
PersistentStorage:持久化存储UI状态
前两个小节介绍的LocalStorage和AppStorage都是运行时的内存,但是在应用退出再次启动后,依然能保存选定的结果,是应用开发中十分常见的现象,这就需要用到PersistentStorage。
PersistentStorage是应用程序中的可选单例对象。此对象的作用是持久化存储选定的AppStorage属性,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。
概述
PersistentStorage将选定的AppStorage属性保留在设备磁盘上。应用程序通过API,以决定哪些AppStorage属性应借助PersistentStorage持久化。UI和业务逻辑不直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问,AppStorage中的更改会自动同步到PersistentStorage。
PersistentStorage和AppStorage中的属性建立双向同步。应用开发通常通过AppStorage访问PersistentStorage,另外还有一些接口可以用于管理持久化属性,但是业务逻辑始终是通过AppStorage获取和设置属性的。
限制条件
PersistentStorage允许的类型和值有:
- number, string, boolean, enum 等简单类型。
- 可以被JSON.stringify()和JSON.parse()重构的对象,但是对象中的成员方法不支持持久化。
- API12及以上支持Map类型,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。且更新的值被持久化存储。详见装饰Map类型变量。
- API12及以上支持Set类型,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。且更新的值被持久化存储。详见装饰Set类型变量。
- API12及以上支持Date类型,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。且更新的值被持久化存储。详见装饰Date类型变量。
- API12及以上支持undefined 和 null。
- API12及以上支持联合类型。
PersistentStorage不允许的类型和值有:
- 不支持嵌套对象(对象数组,对象的属性是对象等)。因为目前框架无法检测AppStorage中嵌套对象(包括数组)值的变化,所以无法写回到PersistentStorage中。
持久化数据是一个相对缓慢的操作,应用程序应避免以下情况:
-
持久化大型数据集。
-
持久化经常变化的变量。
PersistentStorage的持久化变量最好是小于2kb的数据,不要大量的数据持久化,因为PersistentStorage写入磁盘的操作是同步的,大量的数据本地化读写会同步在UI线程中执行,影响UI渲染性能。如果开发者需要存储大量的数据,建议使用数据库api。
PersistentStorage和UI实例相关联,持久化操作需要在UI实例初始化成功后(即loadContent传入的回调被调用时)才可以被调用,早于该时机调用会导致持久化失败。
// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
return;
}
PersistentStorage.persistProp('aProp', 47);
});
}
Environment:设备环境查询
开发者如果需要应用程序运行的设备的环境参数,以此来作出不同的场景判断,比如多语言,深浅色模式等,需要用到Environment设备环境查询。
Environment是ArkUI框架在应用程序启动时创建的单例对象。它为AppStorage提供了一系列描述应用程序运行状态的属性。Environment的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型。
Environment内置参数
键 | 数据类型 | 描述 |
---|---|---|
accessibilityEnabled | boolean | 获取无障碍屏幕读取是否启用。 |
colorMode | ColorMode | 色彩模型类型:选项为ColorMode.LIGHT: 浅色,ColorMode.DARK: 深色。 |
fontScale | number | 字体大小比例,范围: [0.85, 1.45]。 |
fontWeightScale | number | 字体粗细程度,范围: [0.6, 1.6]。 |
layoutDirection | LayoutDirection | 布局方向类型:包括LayoutDirection.LTR: 从左到右,LayoutDirection.RTL: 从右到左。 |
languageCode | string | 当前系统语言值,取值必须为小写字母, 例如zh。 |
其他状态管理
@Watch装饰器:状态变量更改通知
@Watch应用于对状态变量的监听。如果开发者需要关注某个状态变量的值是否改变,可以使用@Watch为状态变量设置回调函数。
概述
@Watch用于监听状态变量的变化,当状态变量变化时,@Watch的回调方法将被调用。@Watch在ArkUI框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。当在严格相等为false的情况下,就会触发@Watch的回调。
装饰器说明
@Watch补充变量装饰器 | 说明 |
---|---|
装饰器参数 | 必填。常量字符串,字符串需要有引号。是(string) => void自定义成员函数的方法的引用。 |
可装饰的自定义组件变量 | 可监听所有装饰器装饰的状态变量。不允许监听常规变量。 |
装饰器的顺序 | 建议@State、@Prop、@Link等装饰器在@Watch装饰器之前。 |
语法说明
类型 | 说明 |
---|---|
(changedPropertyName? : string) => void | 该函数是自定义组件的成员函数,changedPropertyName是被watch的属性名。 在多个状态变量绑定同一个@Watch的回调方法的时候,可以通过changedPropertyName进行不同的逻辑处理 将属性名作为字符串输入参数,不返回任何内容。 |
观察变化和行为表现
-
当观察到状态变量的变化(包括双向绑定的AppStorage和LocalStorage中对应的key发生的变化)的时候,对应的@Watch的回调方法将被触发;
-
@Watch方法在自定义组件的属性变更之后同步执行;
-
如果在@Watch的方法里改变了其他的状态变量,也会引起状态变更和@Watch的执行;
-
在第一次初始化的时候,@Watch装饰的方法不会被调用,即认为初始化不是状态变量的改变。只有在后续状态改变时,才会调用@Watch回调方法。
限制条件
-
建议开发者避免无限循环。循环可能是因为在@Watch的回调方法里直接或者间接地修改了同一个状态变量引起的。为了避免循环的产生,建议不要在@Watch的回调方法里修改当前装饰的状态变量;
-
开发者应关注性能,属性值更新函数会延迟组件的重新渲染(具体请见上面的行为表现),因此,回调函数应仅执行快速运算;
-
不建议在@Watch函数中调用async await,因为@Watch设计的用途是为了快速的计算,异步行为可能会导致重新渲染速度的性能问题。
$$语法:内置组件双向同步
$$运算符为系统内置组件提供TS变量的引用,使得TS变量和系统内置组件的内部状态保持同步。
内部状态具体指什么取决于组件。例如,TextInput组件的text参数。
说明
$$还用于@Builder装饰器的按引用传递参数,开发者需要注意两种用法的区别。
使用规则
-
当前$$支持的组件:
组件 支持的参数/属性 起始API版本 Checkbox select 10 CheckboxGroup selectAll 10 DatePicker selected 10 TimePicker selected 10 MenuItem selected 10 Panel mode 10 Radio checked 10 Rating rating 10 Search value 10 SideBarContainer showSideBar 10 Slider value 10 Stepper index 10 Swiper index 10 Tabs index 10 TextArea text 10 TextInput text 10 TextPicker selected、value 10 Toggle isOn 10 AlphabetIndexer selected 10 Select selected、value 10 BindSheet isShow 10 BindContentCover isShow 10 Refresh refreshing 8 GridItem selected 10 ListItem selected 10 -
$$绑定的变量变化时,会触发UI的同步刷新。
@Track装饰器:class对象属性级更新
@Track应用于class对象的属性级更新。@Track装饰的属性变化时,只会触发该属性关联的UI更新。
概述
@Track是class对象的属性装饰器。当一个class对象是状态变量时,@Track装饰的属性发生变化,只会触发该属性关联的UI更新;而未被标记的属性不能在UI中使用。
装饰器说明
@Track变量装饰器 | 说明 |
---|---|
装饰器参数 | 无 |
可装饰的变量 | class对象的非静态成员属性。 |
观察变化和行为表现
当一个class对象是状态变量时,@Track装饰的属性发生变化,该属性关联的UI触发更新。
说明
当class对象中没有一个属性被标记@Track,行为与原先保持不变。@Track没有深度观测的功能。
使用@Track装饰器可以避免冗余刷新。
class LogTrack {
@Track str1: string;
@Track str2: string;
constructor(str1: string) {
this.str1 = str1;
this.str2 = 'World';
}
}
class LogNotTrack {
str1: string;
str2: string;
constructor(str1: string) {
this.str1 = str1;
this.str2 = '世界';
}
}
@Entry
@Component
struct AddLog {
@State logTrack: LogTrack = new LogTrack('Hello');
@State logNotTrack: LogNotTrack = new LogNotTrack('你好');
isRender(index: number) {
console.log(`Text ${index} is rendered`);
return 50;
}
build() {
Row() {
Column() {
Text(this.logTrack.str1) // UINode1
.fontSize(this.isRender(1))
.fontWeight(FontWeight.Bold)
Text(this.logTrack.str2) // UINode2
.fontSize(this.isRender(2))
.fontWeight(FontWeight.Bold)
Button('change logTrack.str1')
.onClick(() => {
this.logTrack.str1 = 'Bye';
})
Text(this.logNotTrack.str1) // UINode3
.fontSize(this.isRender(3))
.fontWeight(FontWeight.Bold)
Text(this.logNotTrack.str2) // UINode4
.fontSize(this.isRender(4))
.fontWeight(FontWeight.Bold)
Button('change logNotTrack.str1')
.onClick(() => {
this.logNotTrack.str1 = '再见';
})
}
.width('100%')
}
.height('100%')
}
}
在上面的示例中:
- 类LogTrack中的属性均被@Track装饰器装饰,点击按钮"change logTrack.str1",此时UINode1刷新,UINode2不刷新,只有一条日志输出,避免了冗余刷新。
Text 1 is rendered
- 类logNotTrack中的属性均未被@Track装饰器装饰,点击按钮"change logNotTrack.str1",此时UINode3、UINode4均会刷新,有两条日志输出,存在冗余刷新。
Text 3 is rendered Text 4 is rendered
限制条件
-
不能在UI中使用非@Track装饰的属性,包括不能绑定在组件上、不能用于初始化子组件,错误的使用将导致JSCrash;可以在非UI中使用非@Track装饰的属性,如事件回调函数中、生命周期函数中等。
-
建议开发者不要混用包含@Track的class对象和不包含@Track的class对象,如联合类型中、类继承中等。
自定义组件冻结功能
自定义组件处于非激活状态时,状态变量将不响应更新,即@Watch不会调用,状态变量关联的节点不会刷新。通过freezeWhenInactive属性来决定是否使用冻结功能,不传参数时默认不使用。支持的场景有:页面路由,TabContent,LazyForEach,Navigation。
MVVM模式
应用通过状态去渲染更新UI是程序设计中相对复杂,但又十分重要的,往往决定了应用程序的性能。程序的状态数据通常包含了数组、对象,或者是嵌套对象组合而成。在这些情况下,ArkUI采取MVVM = Model + View + ViewModel模式,其中状态管理模块起到的就是ViewModel的作用,将数据与视图绑定在一起,更新数据的时候直接更新视图。
-
Model层:存储数据和相关逻辑的模型。它表示组件或其他相关业务逻辑之间传输的数据。Model是对原始数据的进一步处理。
-
View层:在ArkUI中通常是@Component装饰组件渲染的UI。
-
ViewModel层:在ArkUI中,ViewModel是存储在自定义组件的状态变量、LocalStorage和AppStorage中的数据。
- 自定义组件通过执行其build()方法或者@Builder装饰的方法来渲染UI,即ViewModel可以渲染View。
- View可以通过相应event handler来改变ViewModel,即事件驱动ViewModel的改变,另外ViewModel提供了@Watch回调方法用于监听状态数据的改变。
- 在ViewModel被改变时,需要同步回Model层,这样才能保证ViewModel和Model的一致性,即应用自身数据的一致性。
- ViewModel结构设计应始终为了适配自定义组件的构建和更新,这也是将Model和ViewModel分开的原因。
目前很多关于UI构造和更新的问题,都是由于ViewModel的设计并没有很好的支持自定义组件的渲染,或者试图去让自定义组件强行适配Model层,而中间没有用ViewModel来进行分离。例如,一个应用程序直接将SQL数据库中的数据读入内存,这种数据模型不能很好的直接适配自定义组件的渲染,所以在应用程序开发中需要适配ViewModel层。
根据上面涉及SQL数据库的示例,应用程序应设计为:
-
Model:针对数据库高效操作的数据模型。
-
ViewModel:针对ArkUI状态管理功能进行高效的UI更新的视图模型。
-
部署 converters/adapters: converters/adapters作用于Model和ViewModel的相互转换。
- converters/adapters可以转换最初从数据库读取的Model,来创建并初始化ViewModel。
- 在应用的使用场景中,UI会通过event handler改变ViewModel,此时converters/adapters需要将ViewModel的更新数据同步回Model。
虽然与强制将UI拟合到SQL数据库模式(MV模式)相比,MVVM的设计比较复杂,但应用程序开发人员可以通过ViewModel层的隔离,来简化UI的设计和实现,以此来收获更好的UI性能。
ViewModel的数据源
ViewModel通常包含多个顶层数据源。@State和@Provide装饰的变量以及LocalStorage和AppStorage都是顶层数据源,其余装饰器都是与数据源做同步的数据。装饰器的选择取决于状态需要在自定义组件之间的共享范围。共享范围从小到大的排序是:
-
@State:组件级别的共享,通过命名参数机制传递,例如:CompA: ({ aProp: this.aProp }),表示传递层级(共享范围)是父子之间的传递。
-
@Provide:组件级别的共享,可以通过key和@Consume绑定,因此不用参数传递,实现多层级的数据共享,共享范围大于@State。
-
LocalStorage:页面级别的共享,可以通过@Entry在当前组件树上共享LocalStorage实例。
-
AppStorage:应用全局的UI状态存储,和应用进程绑定,在整个应用内的状态数据的共享。
@State装饰的变量与一个或多个子组件共享状态数据
@State可以初始化多种状态变量,@Prop、@Link和@ObjectLink可以和其建立单向或双向同步,详情见@State使用规范。
-
使用Parent根节点中@State装饰的testNum作为ViewModel数据项。将testNum传递给其子组件LinkChild和Sibling。
// xxx.ets @Entry @Component struct Parent { @State @Watch("testNumChange1") testNum: number = 1; testNumChange1(propName: string): void { console.log(`Parent: testNumChange value ${this.testNum}`) } build() { Column() { LinkChild({ testNum: $testNum }) Sibling({ testNum: $testNum }) } } }
-
LinkChild和Sibling中用@Link和父组件的数据源建立双向同步。其中LinkChild中创建了LinkLinkChild和PropLinkChild。
@Component struct Sibling { @Link @Watch("testNumChange") testNum: number; testNumChange(propName: string): void { console.log(`Sibling: testNumChange value ${this.testNum}`); } build() { Text(`Sibling: ${this.testNum}`) } } @Component struct LinkChild { @Link @Watch("testNumChange") testNum: number; testNumChange(propName: string): void { console.log(`LinkChild: testNumChange value ${this.testNum}`); } build() { Column() { Button('incr testNum') .onClick(() => { console.log(`LinkChild: before value change value ${this.testNum}`); this.testNum = this.testNum + 1 console.log(`LinkChild: after value change value ${this.testNum}`); }) Text(`LinkChild: ${this.testNum}`) LinkLinkChild({ testNumGrand: $testNum }) PropLinkChild({ testNumGrand: this.testNum }) } .height(200).width(200) } }
-
LinkLinkChild和PropLinkChild声明如下,PropLinkChild中的@Prop和其父组件建立单向同步关系。
@Component struct LinkLinkChild { @Link @Watch("testNumChange") testNumGrand: number; testNumChange(propName: string): void { console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`); } build() { Text(`LinkLinkChild: ${this.testNumGrand}`) } } @Component struct PropLinkChild { @Prop @Watch("testNumChange") testNumGrand: number = 0; testNumChange(propName: string): void { console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); } build() { Text(`PropLinkChild: ${this.testNumGrand}`) .height(70) .backgroundColor(Color.Red) .onClick(() => { this.testNumGrand += 1; }) } }
当LinkChild中的@Link testNum更改时。
-
更改首先同步到其父组件Parent,然后更改从Parent同步到Sibling。
-
LinkChild中的@Link testNum更改也同步给子组件LinkLinkChild和PropLinkChild。
@State装饰器与@Provide、LocalStorage、AppStorage的区别:
- @State如果想要将更改传递给孙子节点,需要先将更改传递给子组件,再从子节点传递给孙子节点。
- 共享只能通过构造函数的参数传递,即命名参数机制CompA: ({ aProp: this.aProp })。
完整的代码示例如下:
@Component struct LinkLinkChild { @Link @Watch("testNumChange") testNumGrand: number; testNumChange(propName: string): void { console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`); } build() { Text(`LinkLinkChild: ${this.testNumGrand}`) } } @Component struct PropLinkChild { @Prop @Watch("testNumChange") testNumGrand: number = 0; testNumChange(propName: string): void { console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); } build() { Text(`PropLinkChild: ${this.testNumGrand}`) .height(70) .backgroundColor(Color.Red) .onClick(() => { this.testNumGrand += 1; }) } } @Component struct Sibling { @Link @Watch("testNumChange") testNum: number; testNumChange(propName: string): void { console.log(`Sibling: testNumChange value ${this.testNum}`); } build() { Text(`Sibling: ${this.testNum}`) } } @Component struct LinkChild { @Link @Watch("testNumChange") testNum: number; testNumChange(propName: string): void { console.log(`LinkChild: testNumChange value ${this.testNum}`); } build() { Column() { Button('incr testNum') .onClick(() => { console.log(`LinkChild: before value change value ${this.testNum}`); this.testNum = this.testNum + 1 console.log(`LinkChild: after value change value ${this.testNum}`); }) Text(`LinkChild: ${this.testNum}`) LinkLinkChild({ testNumGrand: $testNum }) PropLinkChild({ testNumGrand: this.testNum }) } .height(200).width(200) } } @Entry @Component struct Parent { @State @Watch("testNumChange1") testNum: number = 1; testNumChange1(propName: string): void { console.log(`Parent: testNumChange value ${this.testNum}`) } build() { Column() { LinkChild({ testNum: $testNum }) Sibling({ testNum: $testNum }) } } }
-
@Provide装饰的变量与任何后代组件共享状态数据
@Provide装饰的变量可以与任何后代组件共享状态数据,其后代组件使用@Consume创建双向同步,详情见@Provide和@Consume。
因此,@Provide-@Consume模式比使用@State-@Link-@Link从父组件将更改传递到孙子组件更方便。@Provide-@Consume适合在单个页面UI组件树中共享状态数据。
使用@Provide-@Consume模式时,@Consume和其祖先组件中的@Provide通过绑定相同的key连接,而不是在组件的构造函数中通过参数来进行传递。
以下示例通过@Provide-@Consume模式,将更改从父组件传递到孙子组件。
@Component
struct LinkLinkChild {
@Consume @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNum value ${this.testNum}`);
}
build() {
Text(`LinkLinkChild: ${this.testNum}`)
}
}
@Component
struct PropLinkChild {
@Prop @Watch("testNumChange") testNumGrand: number = 0;
testNumChange(propName: string): void {
console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`PropLinkChild: ${this.testNumGrand}`)
.height(70)
.backgroundColor(Color.Red)
.onClick(() => {
this.testNumGrand += 1;
})
}
}
@Component
struct Sibling {
@Consume @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`Sibling: testNumChange value ${this.testNum}`);
}
build() {
Text(`Sibling: ${this.testNum}`)
}
}
@Component
struct LinkChild {
@Consume @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`LinkChild: testNumChange value ${this.testNum}`);
}
build() {
Column() {
Button('incr testNum')
.onClick(() => {
console.log(`LinkChild: before value change value ${this.testNum}`);
this.testNum = this.testNum + 1
console.log(`LinkChild: after value change value ${this.testNum}`);
})
Text(`LinkChild: ${this.testNum}`)
LinkLinkChild({ /* empty */ })
PropLinkChild({ testNumGrand: this.testNum })
}
.height(200).width(200)
}
}
@Entry
@Component
struct Parent {
@Provide @Watch("testNumChange1") testNum: number = 1;
testNumChange1(propName: string): void {
console.log(`Parent: testNumChange value ${this.testNum}`)
}
build() {
Column() {
LinkChild({ /* empty */ })
Sibling({ /* empty */ })
}
}
}
给LocalStorage实例中对应的属性建立双向或单向同步
通过@LocalStorageLink和@LocalStorageProp,给LocalStorage实例中的属性建立双向或单向同步。可以将LocalStorage实例视为@State变量的Map,使用详情参考LocalStorage。
LocalStorage对象可以在ArkUI应用程序的几个页面上共享。因此,使用@LocalStorageLink、@LocalStorageProp和LocalStorage可以在应用程序的多个页面上共享状态。
以下示例中:
-
创建一个LocalStorage实例,并通过@Entry(storage)将其注入根节点。
-
在Parent组件中初始化@LocalStorageLink("testNum")变量时,将在LocalStorage实例中创建testNum属性,并设置指定的初始值为1,即@LocalStorageLink("testNum") testNum: number = 1。
-
在其子组件中,都使用@LocalStorageLink或@LocalStorageProp绑定同一个属性名key来传递数据。
LocalStorage可以被认为是@State变量的Map,属性名作为Map中的key。
@LocalStorageLink和LocalStorage中对应的属性的同步行为,和@State和@Link一致,都为双向数据同步。
以下为组件的状态更新图:
@Component
struct LinkLinkChild {
@LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNum value ${this.testNum}`);
}
build() {
Text(`LinkLinkChild: ${this.testNum}`)
}
}
@Component
struct PropLinkChild {
@LocalStorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1;
testNumChange(propName: string): void {
console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`PropLinkChild: ${this.testNumGrand}`)
.height(70)
.backgroundColor(Color.Red)
.onClick(() => {
this.testNumGrand += 1;
})
}
}
@Component
struct Sibling {
@LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`Sibling: testNumChange value ${this.testNum}`);
}
build() {
Text(`Sibling: ${this.testNum}`)
}
}
@Component
struct LinkChild {
@LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`LinkChild: testNumChange value ${this.testNum}`);
}
build() {
Column() {
Button('incr testNum')
.onClick(() => {
console.log(`LinkChild: before value change value ${this.testNum}`);
this.testNum = this.testNum + 1
console.log(`LinkChild: after value change value ${this.testNum}`);
})
Text(`LinkChild: ${this.testNum}`)
LinkLinkChild({ /* empty */ })
PropLinkChild({ /* empty */ })
}
.height(200).width(200)
}
}
// create LocalStorage object to hold the data
const storage = new LocalStorage();
@Entry(storage)
@Component
struct Parent {
@LocalStorageLink("testNum") @Watch("testNumChange1") testNum: number = 1;
testNumChange1(propName: string): void {
console.log(`Parent: testNumChange value ${this.testNum}`)
}
build() {
Column() {
LinkChild({ /* empty */ })
Sibling({ /* empty */ })
}
}
}
给AppStorage中对应的属性建立双向或单向同步
AppStorage是LocalStorage的单例对象,ArkUI在应用程序启动时创建该对象,在页面中使用@StorageLink和@StorageProp为多个页面之间共享数据,具体使用方法和LocalStorage类似。
也可以使用PersistentStorage将AppStorage中的特定属性持久化到本地磁盘的文件中,再次启动的时候@StorageLink和@StorageProp会恢复上次应用退出的数据。详情请参考PersistentStorage文档。
示例如下:
@Component
struct LinkLinkChild {
@StorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNum value ${this.testNum}`);
}
build() {
Text(`LinkLinkChild: ${this.testNum}`)
}
}
@Component
struct PropLinkChild {
@StorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1;
testNumChange(propName: string): void {
console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`PropLinkChild: ${this.testNumGrand}`)
.height(70)
.backgroundColor(Color.Red)
.onClick(() => {
this.testNumGrand += 1;
})
}
}
@Component
struct Sibling {
@StorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`Sibling: testNumChange value ${this.testNum}`);
}
build() {
Text(`Sibling: ${this.testNum}`)
}
}
@Component
struct LinkChild {
@StorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`LinkChild: testNumChange value ${this.testNum}`);
}
build() {
Column() {
Button('incr testNum')
.onClick(() => {
console.log(`LinkChild: before value change value ${this.testNum}`);
this.testNum = this.testNum + 1
console.log(`LinkChild: after value change value ${this.testNum}`);
})
Text(`LinkChild: ${this.testNum}`)
LinkLinkChild({ /* empty */
})
PropLinkChild({ /* empty */
})
}
.height(200).width(200)
}
}
@Entry
@Component
struct Parent {
@StorageLink("testNum") @Watch("testNumChange1") testNum: number = 1;
testNumChange1(propName: string): void {
console.log(`Parent: testNumChange value ${this.testNum}`)
}
build() {
Column() {
LinkChild({ /* empty */
})
Sibling({ /* empty */
})
}
}
}
ViewModel的嵌套场景
大多数情况下,ViewModel数据项都是复杂类型的,例如,对象数组、嵌套对象或者这些类型的组合。对于嵌套场景,可以使用@Observed搭配@Prop或者@ObjectLink来观察变化。
@Prop和@ObjectLink嵌套数据结构
推荐设计单独的自定义组件来渲染每一个数组或对象。此时,对象数组或嵌套对象(属性是对象的对象称为嵌套对象)需要两个自定义组件,一个自定义组件呈现外部数组/对象,另一个自定义组件呈现嵌套在数组/对象内的类对象。 @State、@Prop、@Link、@ObjectLink装饰的变量只能观察到第一层的变化。
-
对于类:
- 可以观察到赋值的变化:this.obj=new ClassObj(...)
- 可以观察到对象属性的更改:this.obj.a=new ClassA(...)
- 不能观察更深层级的属性更改:this.obj.a.b = 47
-
对于数组:
- 可以观察到数组的整体赋值:this.arr=[...]
- 可以观察到数据项的删除、插入和替换:this.arr[1] = new ClassA()、this.arr.pop()、 this.arr.push(new ClassA(...))、this.arr.sort(...)
- 不能观察更深层级的数组变化:this.arr[1].b = 47
如果要观察嵌套类的内部对象的变化,可以使用@ObjectLink或@Prop。优先考虑@ObjectLink,其通过嵌套对象内部属性的引用初始化自身。@Prop会对嵌套在内部的对象的深度拷贝来进行初始化,以实现单向同步。在性能上@Prop的深度拷贝比@ObjectLink的引用拷贝慢很多。
@ObjectLink或@Prop可以用来存储嵌套内部的类对象,该类必须用@Observed类装饰器装饰,否则类的属性改变并不会触发更新,UI并不会刷新。@Observed为其装饰的类实现自定义构造函数,此构造函数创建了一个类的实例,并使用ES6代理包装(由ArkUI框架实现),拦截装饰class属性的所有“get”和“set”。“set”观察属性值,当发生赋值操作时,通知ArkUI框架更新。“get”收集哪些UI组件依赖该状态变量,实现最小化UI更新。
如果嵌套场景中,嵌套数据内部是数组或者class时,需根据以下场景使用@Observed类装饰器。
-
如果嵌套数据内部是class,直接被@Observed装饰。
-
如果嵌套数据内部是数组,可以通过以下方式来观察数组变化。
@Observed class ObservedArray<T> extends Array<T> { constructor(args: T[]) { if (args instanceof Array) { super(...args); } else { super(args) } } /* otherwise empty */ }
ViewModel为外层class。
class Outer { innerArrayProp : ObservedArray<string> = []; ... }
嵌套数据结构中@Prop和@ObjectLink之间的区别
以下示例中:
-
父组件ViewB渲染@State arrA:Array<ClassA>。@State可以观察新数组的分配、数组项插入、删除和替换。
-
子组件ViewA渲染每一个ClassA的对象。
-
类装饰器@Observed ClassA与@ObjectLink a: ClassA。
-
可以观察嵌套在Array内的ClassA对象的变化。
-
不使用@Observed时:
ViewB中的this.arrA[Math.floor(this.arrA.length/2)].c=10将不会被观察到,相应的ViewA组件也不会更新。
对于数组中的第一个和第二个数组项,每个数组项都初始化了两个ViewA的对象,渲染了同一个ViewA实例。在一个ViewA中的属性赋值this.a.c += 1;时不会引发另外一个使用同一个ClassA初始化的ViewA的渲染更新。
-
let NextID: number = 1;
// 类装饰器@Observed装饰ClassA
@Observed
class ClassA {
public id: number;
public c: number;
constructor(c: number) {
this.id = NextID++;
this.c = c;
}
}
@Component
struct ViewA {
@ObjectLink a: ClassA;
label: string = "ViewA1";
build() {
Row() {
Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`)
.onClick(() => {
// 改变对象属性
this.a.c += 1;
})
}
}
}
@Entry
@Component
struct ViewB {
@State arrA: ClassA[] = [new ClassA(0), new ClassA(0)];
build() {
Column() {
ForEach(this.arrA,
(item: ClassA) => {
ViewA({ label: `#${item.id}`, a: item })
},
(item: ClassA): string => { return item.id.toString(); }
)
Divider().height(10)
if (this.arrA.length) {
ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] })
ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] })
}
Divider().height(10)
Button(`ViewB: reset array`)
.onClick(() => {
// 替换整个数组,会被@State this.arrA观察到
this.arrA = [new ClassA(0), new ClassA(0)];
})
Button(`array push`)
.onClick(() => {
// 数组中插入数据,会被@State this.arrA观察到
this.arrA.push(new ClassA(0))
})
Button(`array shift`)
.onClick(() => {
// 数组中移除数据,会被@State this.arrA观察到
this.arrA.shift()
})
Button(`ViewB: chg item property in middle`)
.onClick(() => {
// 替换数组中的某个元素,会被@State this.arrA观察到
this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11);
})
Button(`ViewB: chg item property in middle`)
.onClick(() => {
// 改变数组中某个元素的属性c,会被ViewA中的@ObjectLink观察到
this.arrA[Math.floor(this.arrA.length / 2)].c = 10;
})
}
}
}
在ViewA中,将@ObjectLink替换为@Prop。
@Component
struct ViewA {
@Prop a: ClassA = new ClassA(0);
label : string = "ViewA1";
build() {
Row() {
Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`)
.onClick(() => {
// change object property
this.a.c += 1;
})
}
}
}
与用@Prop装饰不同,用@ObjectLink装饰时,点击数组的第一个或第二个元素,后面两个ViewA会发生同步的变化。
@Prop是单向数据同步,ViewA内的Button只会触发Button自身的刷新,不会传播到其他的ViewA实例中。在ViewA中的ClassA只是一个副本,并不是其父组件中@State arrA : Array<ClassA>中的对象,也不是其他ViewA的ClassA,这使得数组的元素和ViewA中的元素表面是传入的同一个对象,实际上在UI上渲染使用的是两个互不相干的对象。
需要注意@Prop和@ObjectLink还有一个区别:@ObjectLink装饰的变量是仅可读的,不能被赋值;@Prop装饰的变量可以被赋值。
-
@ObjectLink实现双向同步,因为它是通过数据源的引用初始化的。
-
@Prop是单向同步,需要深拷贝数据源。
-
对于@Prop赋值新的对象,就是简单地将本地的值覆写,但是对于实现双向数据同步的@ObjectLink,覆写新的对象相当于要更新数据源中的数组项或者class的属性,这个对于 TypeScript/JavaScript是不能实现的。