组件的状态
@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 了。
@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 装饰的变量和父组件建立双向的同步关系,子组件可以修改值,修改后的变化也会同步回父组件,同样,@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 页面才会收到更新回调。