Harmony ArkTS 状态管理

组件的状态

@State

@State 装饰的变量,即为状态变量,当状态发生改变时,对应的 UI 也会发生改变,和该状态变量不相关的 UI 不会发生重新渲染,从而实现页面渲染的按需更新。它是私有的,只能从组件内部访问,在声明时必须指定其类型和本地初始化。

举个简单的例子,这里点击 Button,改变状态变量,进而改变 Text 的显示。

ts 复制代码
@Entry
@Component
struct Index {
  @State count: number = 1;

  build() {
    Column() {
      Text(this.count.toString())
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
      Button('increase', { type: ButtonType.Capsule }).onClick(() => {
        this.count++
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

但是,并不是所有的更改都会引起 UI 刷新,只有可以被框架观察到的修改才会引起 UI 刷新,那什么样的修改才能被观察到呢?

@State 装饰的数据类型是 string,number,boolean,array,class,Object 时,都能被观察到,其中包含自身的赋值变化和其属性的赋值变化,以及数组本身的增删改变化。

ts 复制代码
export class MyCounter {
  value: number = 1

  constructor(value: number) {
    this.value = value
  }
}
ts 复制代码
@Entry
@Component
struct Index {
  @State counter: MyCounter = new MyCounter(1)

  build() {
    Column() {
      Text(this.counter.value.toString())
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
      Button('increase', { type: ButtonType.Capsule }).onClick(() => {
        this.counter.value++
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

对于 class 或 Object,嵌套属性的赋值观察不到

ts 复制代码
export class MyCounter {
  son: MyCounterSon;

  constructor(son: MyCounterSon) {
    this.son = son
  }
}

export class MyCounterSon {
  value: number = 1

  constructor(value: number) {
    this.value = value
  }
}
ts 复制代码
@Entry
@Component
struct Index {
  @State counter: MyCounter = new MyCounter(new MyCounterSon(2))

  build() {
    Column() {
      Text(this.counter.son.value.toString())
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
      Button('increase', { type: ButtonType.Capsule }).onClick(() => {
        this.counter.son.value++
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

this.counter.son.value++ 是嵌套属性的赋值,它无法被观察到,所以点击按钮是不会更新 UI 的。

对于 array,数组项中属性的赋值观察不到。

ts 复制代码
@State counter: MyCounter[] = [
  new MyCounter(new MyCounterSon(1)),
  new MyCounter(new MyCounterSon(2))
]

同样,执行自增,虽然他实际的值会变,但是它也是不能被观察到的,自然就不会引起 UI 的刷新。

ts 复制代码
this.counter[0].son.value++

当装饰的变量类型为 Map,Date,Set 时,可以观察到其整体的赋值,同时也可以通过相应的接口更新其值,比如 Map 的 set,Date 的 setDate,Set 的 add 等。

ts 复制代码
@Entry
@Component
struct Index {
  @State countMap: Map<string, number> = new Map([
    ['key1', 1],
    ['key2', 2]
  ])

  build() {
    Column() {
      Text(this.countMap.get('key1')?.toString())
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
      Button('increase', { type: ButtonType.Capsule }).onClick(() => {
        this.countMap.set('key1', 8)
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

@State 支持联合类型和 undefined 和 null

ts 复制代码
@State count: number | undefined = 1

@Prop

@Prop 装饰的变量和父组件建立单向的同步关系,它允许在本地修改,但修改后的变化不会同步回父组件,不能在 @Entry 装饰的自定义组件中使用。

ts 复制代码
@Component
export struct SonComponent {
  @Prop msg: string = ''

  build() {
    Text(this.msg).fontSize(50)
  }
}
ts 复制代码
@Entry
@Component
struct Index {
  @State parentMsg: string = 'hello huawei'

  build() {
    Column() {
      SonComponent({ msg: this.parentMsg })
      Button('change', { type: ButtonType.Capsule }).onClick(() => {
        this.parentMsg = 'be far ahead'
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

对于 Object 或 class 复杂类型,只能观察到第一层的属性变化,不能观察到第二层的属性变化。

ts 复制代码
export class Parent {
  name: string
  child: Child

  constructor(name: string, child: Child) {
    this.name = name;
    this.child = child;
  }
}

export class Child {
  name: string

  constructor(name: string) {
    this.name = name;
  }
}

创建一个子组件

ts 复制代码
@Component
export struct SonComponent {
  @Prop child: Child

  build() {
    Column() {
      Text(`SonComponent -> ${this.child.name}`).fontSize(20)
    }
  }
}

将父组件的状态变量传递到子组件

ts 复制代码
@Entry
@Component
struct Index {
  @State parentData: Parent = new Parent('parent_name', new Child('son_name'))

  build() {
    Column() {
      Text()
      Text(`ParentComponent -> ${this.parentData.child.name}`).fontSize(20)
      SonComponent({ child: this.parentData.child })
      Button('Change', { type: ButtonType.Capsule }).onClick(() => {
        // 无法被观察到
        this.parentData.child.name = 'huawei'
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

执行后发现,点击按钮没有引起 UI 更新,这是第二层属性的变化,无论是父组件还是子组件都无法观察到。由于只能观察到第一层属性的赋值,所以我们可以这样更新值。

ts 复制代码
this.parent.child = new Child('huawei')

如果不想这么处理,就希望能跨层级属性赋值的话,就应该使用 @Observed 和 @ObjectLink 了。

如果需要在嵌套对象或数组的场景中进行双向数据同步,就可以使用 @ObjectLink 和 @Observed,被 @Observed 装饰就可以被观察到属性的变化。子组件中 @ObjectLink 装饰的状态变量用于接收 @Observed 装饰的类实例,沿用并修改上面的例子,使其点击按钮后修改的值能被观察到。

ts 复制代码
@Observed
export class Parent {
  name: string
  child: Child

  constructor(name: string, child: Child) {
    this.name = name;
    this.child = child;
  }
}

@Observed
export class Child {
  name: string

  constructor(name: string) {
    this.name = name;
  }
}
ts 复制代码
@Component
export struct SonComponent {
  @ObjectLink child: Child

  build() {
    Column() {
      Text(`SonComponent -> ${this.child.name}`).fontSize(20)
    }
  }
}
ts 复制代码
@Entry
@Component
struct Index {
  @State parentData: Parent = new Parent('parent_name', new Child('son_name'))

  build() {
    Column() {
      Text()
      Text(`ParentComponent -> ${this.parentData.child.name}`).fontSize(20)
      SonComponent({ child: this.parentData.child })
      Button('Change', { type: ButtonType.Capsule }).onClick(() => {
        this.parentData.child.name = 'huawei'
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

@Link 装饰的变量和父组件建立双向的同步关系,子组件可以修改值,修改后的变化也会同步回父组件,同样,@Link 装饰器不能在 @Entry 装饰的自定义组件中使用。

ts 复制代码
@Component
export struct SonComponent {
  @Link parentData: number[]

  build() {
    Column() {
      Text(`SonComponent: ${this.parentData}`).fontSize(20)
      Button('change', { type: ButtonType.Capsule }).onClick(() => {
        this.parentData = [7, 8, 9]
      })
    }
  }
}
ts 复制代码
@Entry
@Component
struct Index {
  @State parentData: number[] = [1, 2, 3]

  build() {
    Column() {
      SonComponent({ parentData: this.parentData })
      Text(`ParentComponent: ${this.parentData}`).fontSize(20)
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

点击子组件的 Button,父组件的 Text 也会跟着刷新。不同的是,@Link 装饰的变量是禁止在本地初始化的,会报错。

@Provide 和 @Consume

@Prop 和 @Link 只能用于父与子组件之间的数据同步,而 @Provide 和 @Consume 可以实现跨层级,与后代组件的双向数据同步。在祖先组件中使用 @Provide 将状态变量提供给后代组件,在后代组件中使用 @Consume 去消费这个状态变量。

ts 复制代码
@Component
export struct SonComponent {
  @Consume parentData: number[]

  build() {
    Column() {
      Text(`SonComponent: ${this.parentData}`).fontSize(20)
      Button('change', { type: ButtonType.Capsule }).onClick(() => {
        this.parentData = [7, 8, 9]
      })
    }
  }
}
ts 复制代码
@Entry
@Component
struct Index {
  @Provide parentData: number[] = [1, 2, 3]

  build() {
    Column() {
      //无需传递变量
      SonComponent()
      Text(`ParentComponent: ${this.parentData}`).fontSize(20)
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

@Consume 装饰的变量禁止本地初始化,@Provide 装饰的状态变量对其所有后代组件都可用,我们不需要在组件之间传递变量,可以通过相同的变量名或别名绑定,例如:

ts 复制代码
@Provide parentData: number[] = [1, 2, 3]
@Consume parentData: number[]
ts 复制代码
@Provide('hello') parentData: number[] = [1, 2, 3]
@Consume('hello') ChildData: number[]

应用的状态

LocalStorage

LocalStorage 是页面级的 UI 状态存储,通过 @Entry 装饰器接收的参数可以在页面内共享同一个 LocalStorage 实例,它有两个相关的装饰器 @LocalStorageProp 和 @LocalStorageLink,这俩装饰器与 LocalStorage 中给定属性分别是单向和双向的同步关系。

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

ts 复制代码
let para: Record<string, number> = { 'Key1': 888 };
let storage: LocalStorage = new LocalStorage(para);
storage.setOrCreate('Key2', 'hello');


@Entry(storage)
@Component
struct Index {
  @LocalStorageLink('Key2') data: string = '';

  build() {
    Column() {
      Text()
      Text(`LocalStorage: ${this.data}`).fontSize(20)
      Button('Change', { type: ButtonType.Capsule }).onClick(() => {
        this.data = 'huawei'
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

AppStorage

AppStorage 是应用全局的 UI 状态存储,在应用启动时创建,它也有两个相关的装饰器 @StorageProp 和 @StorageLink,这俩装饰器与 AppStorage 中给定属性分别是单向和双向的同步关系。

ts 复制代码
AppStorage.setOrCreate('Key', 'hello');


@Entry
@Component
struct Index {
  @StorageProp('Key') data: string = '';

  build() {
    Column() {
      Text()
      Text(`AppStorage: ${this.data}`).fontSize(20)
      Button('Change', { type: ButtonType.Capsule }).onClick(() => {
        this.data = 'huawei'
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

PersistentStorage

如果想在应用退出后再次启动依然能保存对应的 UI 状态,就可以使用 PersistentStorage。PersistentStorage 将选定的 AppStorage 属性保留在设备磁盘上。

UI 和业务逻辑不直接访问 PersistentStorage 中的属性,所有属性访问都是对 AppStorage 的访问,AppStorage 中的更改会自动同步到 PersistentStorage。

ts 复制代码
PersistentStorage.persistProp('Key', new User('8', 'Jack'));
//通过 AppStorage 获取值
AppStorage.get<User>('key')

@Entry
@Component
struct Index {
  @StorageLink('Key') data: User = new User('0', '0');

  build() {
    Column() {
      Text()
      Text(`PersistentStorage: ${JSON.stringify(this.data)}`).fontSize(20)
      Button('Change', { type: ButtonType.Capsule }).onClick(() => {
        this.data = new User('10', 'Tom')
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

其他状态

@Watch

@Watch 用于监听状态变量的变化,当状态变量变化时,@Watch 的回调方法将被调用。需要注意地是,在第一次初始化的时候,@Watch 装饰的方法不会被调用,只有在后续状态改变时,才会回调。

ts 复制代码
@State @Watch('isConnected') connect: boolean = false

/**
 *
 * @param changedPropertyName:被 watch 的属性名,此处就是 connect,当多个状态变量绑定同一个 @Watch 方
 * 法时,就可以根据这个属性名去做不同的逻辑处理。
 */
isConnected(changedPropertyName: string) {

}

只有值改变了,@Watch 方法才会被回调,ArkUI 使用的是严格相等(===),举个例子:

ts 复制代码
@Entry
@Component
struct Index {
  @State @Watch('contentChange') content: string = ''

  contentChange(changedPropertyName: string) {
    console.log('changedPropertyName: ' + this.content)
  }

  build() {
    Column() {
      Button('Change', { type: ButtonType.Capsule }).onClick(() => {
        this.content = 'hello harmony'
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

这段代码,无论我点击多少次按钮,contentChange 方法只会被调用一次,因为赋的值没变。

组件冻结

先来看段代码

ts 复制代码
AppStorage.setOrCreate('Info', 'huawei')
ts 复制代码
@Entry
@Component
struct Index {
  @StorageLink('Info') @Watch('contentChange') info: string = ''

  contentChange(changedPropertyName: string) {
    console.log('changedPropertyName: ' + changedPropertyName)
  }

  build() {
    Column() {
      Button('Jump', { type: ButtonType.Capsule }).onClick(() => {
        router.pushUrl({ url: 'pages/MyPage' })
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

点击 Jump Button,跳转到另一个页面。

ts 复制代码
@Entry
@Component
struct MyPage {
  @StorageLink('Info') info: string = ''

  build() {
    Column() {
      Text(this.info).fontSize(20)
      Button('change value', { type: ButtonType.Capsule }).onClick(() => {
        this.info = 'google'
      })
      Button('back', { type: ButtonType.Capsule }).onClick(() => {
        router.back()
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

在 MyPage 页面更新 info 的值,即使 Index 页面处于不可见状态,也会收到相应的更新回调。如果想让页面在非活跃状态下不响应更新,就可以使用冻结功能 freezeWhenInactive。

less 复制代码
@Entry
@Component({ freezeWhenInactive: true })
struct Index {
  ......
}

这样,在 MyPage 页面更新值,Index 页面就不会收到更新通知了,只有当点击 back 按钮返回 Index 页面时,Index 页面才会收到更新回调。

相关推荐
dawn5 小时前
鸿蒙ArkTS中的获取网络数据
华为·harmonyos
桃花键神5 小时前
鸿蒙5.0时代:原生鸿蒙应用市场引领开发者服务新篇章
华为·harmonyos
鸿蒙自习室5 小时前
鸿蒙多线程开发——并发模型对比(Actor与内存共享)
华为·harmonyos
JavaPub-rodert7 小时前
鸿蒙生态崛起:开发者的机遇与挑战
华为·harmonyos
帅比九日9 小时前
【HarmonyOS Next】封装一个网络请求模块
前端·harmonyos
yilylong10 小时前
鸿蒙(Harmony)实现滑块验证码
华为·harmonyos·鸿蒙
baby_hua10 小时前
HarmonyOS第一课——DevEco Studio的使用
华为·harmonyos
HarmonyOS_SDK11 小时前
融合虚拟与现实,AR Engine为用户提供沉浸式交互体验
harmonyos
- 羊羊不超越 -12 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
长弓三石14 小时前
鸿蒙网络编程系列44-仓颉版HttpRequest上传文件示例
前端·网络·华为·harmonyos·鸿蒙