状态管理
在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。
自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。 下图展示了State和View(UI)之间的关系。
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-state-management-overview-0000001524537145-V2

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。
管理组件拥有的状态
@State装饰器:组件内状态
@state 装饰器标记的变量必须初始化, 不能为空值
@state 支持Object、class、string、number、boolean、enum类型,以及这些类型的数组。不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。
嵌套类型以及数组中的对象属性无法触发视图更新
class Person {
name: string
age: number
gf: Person
constructor(name: string, age: number, gf?: Person) {
this.age = age
this.name = name
this.gf = gf
}
}
@Entry
@Component
struct Second {
@State p: Person = new Person('jack', 21, new Person('aaa', 11))
build() {
Column() {
Text(`${this.p.name}: ${this.p.age}`)
.fontSize(50)
.onClick(() => {
this.p.age ++
})
Text(`${this.p.gf.name}: ${this.p.gf.age}`)
.fontSize(50)
.onClick(() => {
console.log(`${this.p.gf.name}: ${this.p.gf.age}`)
this.p.gf.age ++
})
}
.width("100%")
}
}

当点击下面时,不会触发视图更新, 只有点击上面非嵌套属性时才会触发视图整体更新

@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变。
在状态变量相关装饰器中,@State是最基础的,使变量拥有状态属性的装饰器,它也是大部分状态变量的数据源。
@State装饰的变量,与声明式范式中的其他被装饰变量一样,是私有的,只能从组件内部访问,在声明时必须指定其类型和本地初始化。初始化也可选择使用命名参数机制从父组件完成初始化。
@State装饰的变量拥有以下特点:
- @State装饰的变量与子组件中的@Prop装饰变量之间建立单向数据同步,与@Link、@ObjectLink装饰变量之间建立双向数据同步。
- @State装饰的变量生命周期与其所属自定义组件的生命周期相同。
案例
class Task {
static id: number = 1
name: string = `Task${Task.id++}`
finished: boolean = false
}
@Styles function card() {
.width("95%")
.padding(20)
.backgroundColor(Color.White)
.borderRadius(15)
.shadow(
{
radius: 6,
color: "#1F000000",
offsetX: 2,
offsetY: 4
}
)
}
@Extend(Text) function finishedTask() {
.decoration({ type: TextDecorationType.LineThrough })
.fontSize("##B1B2B1")
}
@Entry
@Component
struct Second {
@State totalTask: number = 0
@State finishTask: number = 0
@State task: Task[] = []
handTaskChange() {
this.totalTask = this.task.length
this.finishTask = this.task.filter(item => item.finished).length
}
build() {
Column({ space: 10 }) {
// 任务进度卡片
Row() {
Text("任务进度: ")
.fontSize(30)
.fontWeight(FontWeight.Bold)
Stack() {
Progress({
value: this.finishTask,
total: this.totalTask,
type: ProgressType.Ring
})
.width(100)
Row() {
Text(this.finishTask.toString())
.fontSize(24)
.fontColor("#36D")
Text("/" + this.totalTask.toString())
.fontSize(24)
}
}
}
.card()
.margin({ top: 20, bottom: 10 })
.justifyContent(FlexAlign.SpaceEvenly)
// 新增任务按钮
Button("添加任务")
.width(200)
.onClick(() => {
this.task.push(new Task())
this.handTaskChange()
})
// 渲染任务列表
List({ space: 10 }) {
ForEach(
this.task,
(item: Task, index) => {
ListItem() {
Row() {
Text(item.name)
.fontSize(24)
Checkbox()
.select(item.finished)
.onChange(value => {
item.finished = value
this.handTaskChange()
})
}
.card()
.justifyContent(FlexAlign.SpaceBetween)
}
.swipeAction({
end: this.DeleteButton(index)
})
}
)
}
.width("100%")
.alignListItem(ListItemAlign.Center)
.layoutWeight(1)
}
.width("100%")
.height("100%")
.backgroundColor("#F1F2F3")
}
// 构建函数
@Builder DeleteButton(index: number) {
Text("Del")
.fontColor(Color.White)
.padding(20)
.backgroundColor(Color.Red)
.borderRadius("50%")
.margin(5)
.onClick(() => {
this.task.splice(index, 1)
this.handTaskChange()
})
}
}

@Prop/ @Link 装饰器
@Prop 父子组件单向同步: @Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。 @Link 父子组件双向同步: 子组件中被@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。@Link装饰的变量与其父组件中的数据源共享相同的值。

在最新版本中, @Prop 可以在子组件初始化, 但是编辑器 eslint 会报错
@Component
struct Child {
@Prop value: number;
build() {
Text(`${this.value}`)
.fontSize(50)
.onClick(() => {
console.log(`${this.value}`)
this.value++
})
}
}
@Entry
@Component
struct Index {
@State arr: number[] = [1, 2, 3];
build() {
Row() {
Column() {
Child({ value: this.arr[0] })
Child({ value: this.arr[1] })
Child({ value: this.arr[2] })
Divider().height(5)
ForEach(this.arr,
item => {
Child({ 'value': item } as Record<string, number>)
},
item => item.toString()
)
Text('replace entire arr')
.fontSize(50)
.onClick(() => {
// 两个数组都包含项“3”。
this.arr = this.arr[0] == 1 ? [3, 4, 5] : [1, 2, 3];
})
}
}
}
}
如果点击界面上的“1”六次、“2”五次、“3”四次,将所有变量的本地取值都变为“7”。
7
7
7
----
7
7
7
单击replace entire arr后,屏幕将显示以下信息,为什么?
3
4
5
----
7
4
5
- 在子组件Child中做的所有的修改都不会同步回父组件Index组件,所以即使6个组件显示都为7,但在父组件Index中,this.arr保存的值依旧是[1,2,3]。
- 点击replace entire arr,this.arr[0] == 1成立,将this.arr赋值为[3, 4, 5];
- 因为this.arr[0]已更改,Child({value: this.arr[0]})组件将this.arr[0]更新同步到实例@Prop装饰的变量。Child({value: this.arr[1]})和Child({value: this.arr[2]})的情况也类似。
- this.arr的更改触发ForEach更新,this.arr更新的前后都有数值为3的数组项:[3, 4, 5] 和[1, 2, 3]。根据diff算法,数组项“3”将被保留,删除“1”和“2”的数组项,添加为“4”和“5”的数组项。这就意味着,数组项“3”的组件不会重新生成,而是将其移动到第一位。所以“3”对应的组件不会更新,此时“3”对应的组件数值为“7”,ForEach最终的渲染结果是“7”,“4”,“5”。
diff算法是一种用于比较两个数据结构(比如两个数组或两个树)之间差异的算法。在前端开发中,diff算法通常用于虚拟DOM的更新和渲染优化。
在React等前端框架中,当数据发生变化时,diff算法可以帮助确定哪些DOM节点需要被更新,哪些需要被添加或删除,以及哪些可以被保留而不进行重新渲染。这可以提高性能并减少不必要的DOM操作。
diff算法通常包括以下步骤:
- 比较两个数据结构的差异,找出新增、删除和更新的部分。
- 标记需要进行更新的部分,并记录其变化。
- 应用这些变化,更新DOM或其他视图。
@Styles function btn() {
.margin(12)
.width(312)
.height(40)
}
@Component
struct Child {
@Link items: number[];
build() {
Column() {
Button(`Button1: push`)
.btn()
.onClick(() => {
this.items.push(this.items.length + 1);
})
Button(`Button2: replace whole item`)
.btn()
.onClick(() => {
this.items = [100, 200, 300];
})
}
}
}
@Entry
@Component
struct Parent {
@State arr: number[] = [1, 2, 3];
build() {
Column() {
Button('Button Parent push')
.btn()
.onClick(() => {
this.arr.push(this.arr.length + 1);
})
Child({ items: $arr })
.margin(12)
ForEach(this.arr,
(item: number) => {
Button(`${item}`)
.margin(12)
.width(312)
.height(40)
.backgroundColor('#11a2a2a2')
.fontColor('#e6000000')
},
(item: ForEachInterface) => item.toString()
)
}
}
}

@Observed装饰器和@ObjectLink装饰器:嵌套类对象属性变化
上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。
以下是引入@Observed/@ObjectLink装饰器的示例:
@Observed
class Person {
name: string
age: number
gf: Person
constructor(name: string, age: number, gf?: Person) {
this.name = name
this.age = age
this.gf = gf
}
}
@Entry
@Component
struct Child {
@State p: Person = new Person("Waite", 18, new Person("www", 11))
@State gfs: Person[] = [
new Person("Jack", 18),
new Person("XiaoMing", 20)
]
build() {
Column({ space: 10 }) {
PersonChild({ p: this.p.gf })
.onClick(() => {
console.log(`${this.p.gf.age}`)
this.p.gf.age++
})
Text("Girl Friends List")
ForEach(
this.gfs,
item => {
PersonChild({ p: item })
.onClick(() => item.age++)
}
)
}
}
}
@Component
struct PersonChild {
@ObjectLink p: Person
build() {
Column() {
Text(`${this.p.name}: ${this.p.age}`)
}
}
}
为了给变量加 @ObjectLink 装饰器, 写成组件 -> 因为装饰器没法装饰一个参数
@Observed class 不管嵌套几个类型, 凡是嵌套的, 都要加上 @Observed 装饰器
完善案例, 当 Checkbox 选中时, 会触发对应的任务完成状态, 任务完成状态改变时, 会触发任务进度的更新
@Observed
class Task {
static id: number = 1
name: string = `Task${Task.id++}`
finished: boolean = false
}
@Styles function card() {
.width("95%")
.padding(20)
.backgroundColor(Color.White)
.borderRadius(15)
.shadow(
{
radius: 6,
color: "#1F000000",
offsetX: 2,
offsetY: 4
}
)
}
@Extend(Text) function finishedTask() {
.fontColor("#B1B2B1")
}
@Entry
@Component
struct Second {
@State totalTask: number = 0
@State finishTask: number = 0
@State task: Task[] = []
handTaskChange() {
this.totalTask = this.task.length
this.finishTask = this.task.filter(item => item.finished).length
}
build() {
Column({ space: 10 }) {
// 任务进度卡片
TaskStatistics({ finishTask: this.finishTask, totalTask: this.totalTask })
// 新增任务按钮
Button("添加任务")
.width(200)
.onClick(() => {
this.task.push(new Task())
this.handTaskChange()
})
// 渲染任务列表
List({ space: 10 }) {
ForEach(
this.task,
(item: Task, index) => {
ListItem() {
itemChild({item: item, onTaskChanged: () => this.handTaskChange()})
}
.swipeAction({
end: this.DeleteButton(index)
})
}
)
}
.width("100%")
.height(0)
.alignListItem(ListItemAlign.Center)
.layoutWeight(1)
}
.width("100%")
.height("100%")
.backgroundColor("#F1F2F3")
}
// 构建函数
@Builder DeleteButton(index: number) {
Text("Del")
.fontColor(Color.White)
.padding(20)
.backgroundColor(Color.Red)
.borderRadius("50%")
.margin(5)
.onClick(() => {
this.task.splice(index, 1)
this.handTaskChange()
})
}
}
@Component
struct TaskStatistics {
@Prop finishTask: number
@Prop totalTask: number
build() {
Row() {
Text("任务进度: ")
.fontSize(30)
.fontWeight(FontWeight.Bold)
Stack() {
Progress({
value: this.finishTask,
total: this.totalTask,
type: ProgressType.Ring
})
.width(100)
Row() {
Text(this.finishTask.toString())
.fontSize(24)
.fontColor("#36D")
Text("/" + this.totalTask.toString())
.fontSize(24)
}
}
}
.card()
.margin({ top: 20, bottom: 10 })
.justifyContent(FlexAlign.SpaceEvenly)
}
}
@Component
struct itemChild {
@ObjectLink item: Task
onTaskChanged: () => void
build(){
Row() {
if (this.item.finished) {
Text(this.item.name)
.finishedTask()
}
else {
Text(this.item.name)
}
Checkbox()
.select(this.item.finished)
.onChange(value => {
this.item.finished = value
this.onTaskChanged()
})
}
.card()
.justifyContent(FlexAlign.SpaceBetween)
}
}