在使用状态管理之前,我们所构建的页面大多数为静态页面,如果希望构建一个动态的,有交互的界面,就要引用‘状态’的概念
状态的概念:
在ArkUI框架中,UI是程序运行的结果,用户构建了一个UI模型,其中运行时的状态为参数,当参数改变时,UI作为返回的结果,也会随之发生了改变,这些运行的变化所带来的UI重新渲染,在ArkUI中统称为状态管理机制(什么是状态管理机制)
自定义组件拥有变量(成员变量),变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染的刷新,如果不使用状态变量,UI只能在初始状态时渲染,后续不在刷新
/* UI(View):UI渲染,将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面
* State:状态,驱动UI更新的数据,用户通过触发组件的事件方法,改变状态数据,状态数据的改变引起UI重新渲染
* render:渲染
* event-handlers:事件函数
*/
@State装饰的变量与子组件中的@Prop装饰的变量之间建立单向数据同步与@Link,@ObjectLink装饰的变量之间形成双向同步
@State装饰的变量的生命周期与其所属自定义组件的生命周期相同
/* 使用规则:
* 同步类型:不与父组件任何的类型变量同步
* 允许装饰的变量类型:
* 基本类似:number,boolean,string
* 引用类似:object,class,enum(枚举)
* 以及这些类型的数组
* AIP11以后
* 允许装饰Set Map
* 允许支持装饰undefined和null
* 允许支持装饰联合类型
* 比如:string | number
* 被装饰的变量的初始值:必须本地初始化
*/
从父组件初始化:可选,可以从父组件初始化或本地初始化,从父组件初始化将会覆盖本地初始化
用于初始化子组件:
@State装饰的变量支持初始化子组件常规变量:@State,@Link,@Prop,@Provide
不支持外部组件访问,只能在组件内部访问
并不是状态变量的所有更改都会引起UI刷新,只有可以被框架观察到的修改才会引起UI刷新
当装饰的数据类型为基本类型(string,number,boolean)可以观察到数值变化
当装饰的数据为class或者Object,可以观察到自身的赋值变化,和其属性的赋值的变化(第一层)嵌套属性的赋值观察不到(第二层)
**ps:**嵌套属性的赋值(第二层)如果和非嵌套(第一层)一起使用[UI结果在同一组件里面]也会触发值的改变(非嵌套属性发生改变,那么嵌套的组件属性也发生改变(前提在同一组件中))
当装饰的对象是array时,可以观察到数组本身(赋值,添加,删除,更新数组的变化) 数组项的值可以观察到,数组项中的属性变化观察不到
// * pop 删除最后一个
// * shift 删除第一个
// * push 向数组最后一个添加
// * unshift 向数组第一个添加
当装饰的对象是Date时,可以观察到Date的整体赋值,同时可通过调用一系列的Date接口(setFullYear…)更新Date属性
// import { ClassA } from './Aug_12_am'
@Entry
@Component
struct Aug_12_pm {
// @State message: string = 'Hello World';
//@State arr:ClassA[] = [new ClassA('a'),new ClassA('b')]
@State _time: Date = new Date()
build() {
Column({space : 20}){
// Text('Time')
// .onClick(() => {
// //当前计算机的时间戳
// // console.log(`${this._time}`)
// //let year = this._time.getFullYear()
// //let month = this._time.getMonth() 比正常月份少1
// //let week = this._time.getDay() 星期几
// //let day = this._time.getDate() 几号
// //let _h = this._time.getHours() 时
// //let _m = this._time.getMinutes() 分
// //let _s = this._time.getSeconds() 秒
// // let _mill = this._time.getMilliseconds()
// //目标时
// // let newDate = new Date("2024/8/12,22:00:00")
// // //此为2024/8/12,22:00:00 距离 1970/1/1 00:00:00 的时间差 单位ms
// // let endTime = newDate.getTime()
// // console.log(`${endTime}`) //1970/1/1 - 2024/8/12 单位:ms
// // //此为当前距离 1970/1/1 00:00:00 的时间差 单位ms
// // let timer = this._time.getTime()
// // //两个值相减即为现在距离晚上10点的时间差
// // console.log(`${((endTime - timer)/1000/3600).toFixed(2)}`)
// //格林威治时间和本地时间之间的时差,以分钟为单位
// // let tomeZone = this._time.getTimezoneOffset()
// // console.log(`${tomeZone}`)
//
//
// })
// .fontSize(30)
//日期选择器组件,用于根据指定日期范围创建日期滑动选择器
Button('选择 2023年8月12日')
.onClick(() => {
this._time = new Date('2023-08-12')
})
Button('选择下一年')
.onClick(() => {
this._time.setFullYear(this._time.getFullYear() + 1)
})
Button('选择下一个月')
.onClick(() => {
this._time.setMonth(this._time.getMonth() + 1)
})
DatePicker({
//start 日历开始时间,默认为1970
start:new Date('1999-03-23'),
//end 日历结束时间 默认为2100
end: new Date('2050-01-01'),
//设置选中项的日期。默认值:当前系统日期
selected:this._time
})
//设置弹窗的日期是否显示农历
.lunar(false)
//事件 选择日期时触发该事件
.onDateChange((value) => {
console.log(`${value}`)
})
//时间选择器组件
// TimePicker()
//倒计时组件
//TextTimer()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
@Entry
@Component
/*
作者:ZhuWei
时间:2024/8/13
*/
struct mytest03 {
@State arr:number[] = [1,2,3]
build() {
Column({space:20}) {
//父组件@State数组项到子组件@Prop简单数据同步
Text(`父组件的number:${JSON.stringify(this.arr)}`)
.fontSize(30)
Child({ num:this.arr[0] })
Child({ num:this.arr[1] })
Child({ num:this.arr[2] })
ForEach(this.arr,(item:number) => {
Child({ num: item })
},(item:string) => item.toString())
Text('更新数组项里面的数据')
.fontSize(30)
.onClick(() => {
/*
最开始的键值: '1' '2' '3'
* [1, 2, 3]
* 生成键值: '3' '4' '5'
* [3, 4, 5]
*
* */
/*
* 初始渲染创6个子组件实例,每个@Prop装饰
* 的变量初始化都在本地拷贝一份数组项。子组件onclick
* 事件处理程序会更该局部变量
* 如果点击 界面上的
* '1' 3下 '2' 3下 '3' 3下 将变量本地的值依次变为 '456456'
* 点击 '更新数组项里面的数据'按钮后 依次变为'345645'
* 在子组件Child中做的所有修改都不会同步会父组件,所以所有子组件不管如何点击
* 但在父组件Index中的arr始终为[1,2,3]
* 点击 '更新数组项里面的数据' 按钮后 this.arr[0] == 1 成立
* this.arr赋值为 3,4,5
* 因为this.arr更改 child({ num:this.arr[] })组件将this.arr
* 更新同步到实例@Prop装饰的变量
* this.arr更改触发ForEach的更新,this.arr更新前后都有数值为3的数组项
* 根据diff算法,同名键值3会被保留,删除 1、2,添加为 4、5
* 这就意味着3不会重新生成,而是移动到更新后的第一位,即原先为3的键值渲染的结果移接掉现在键值为3的键值中
*
*
* */
this.arr =
this.arr[0] == 1 ?
[3, 4, 5] : [1, 2, 3]
})
}
.height('100%')
.width('100%')
}
}
//父组件@State数组项到子组件@Prop简单数据同步
@Component
struct Child{
@Prop num:number
// @Prop title:Model
build() {
Column(){
Text(`子组件的number:${this.num}`)
.onClick(() => {
this.num++
})
.fontSize(30)
// Text(`${JSON.stringify(this.title)}`)
// .fontSize(30)
}
}
}
let nextId: number = 1
@Observed
class Book{
id: number
title: string
pages: number
read: boolean = false
constructor(title: string, pages: number) {
this.id = nextId++
this.title = title
this.pages = pages
}
}
/*
* 案例知识点:
* (1):?:判断的的嵌套
* (2):@Prop,的变更不会影响到父组件的变更(数据单向传递),而父组件的刷新会引起子组件的重新刷新渲染
* (3):直接修改第二层的数据(被数组包裹的类里面的属性)是无法被父组件观察到的,需要使用@Observed装饰class Book,Book属性被观察到
* (4)@Observed装饰的类的实例会被不透明的代理对象包装,此代理可以检测到包装对象内的所有属性更改,如果发生这种情况,代理通知@Prop,@Prop对象的值跟新
*
*
* */
@Entry
@Component
/*
作者:ZhuWei
时间:2024/8/13
*/
struct mytest01 {
@State message: string = 'Hello World';
@State allBooks:Book[] = [
new Book("母猪的产后护理",700),
new Book("如何获取富婆欢心",800),
new Book("霸道总裁爱上我",1000)
]
build() {
Column({space:20}) {
Text('第一位借阅者')
.fontSize(30)
.fontWeight(800)
// 展示数组中第一本书
Reader({ book: this.allBooks[0] })
Text(`父组件的借阅者${JSON.stringify(this.allBooks[0])}`)
.fontSize(30)
Text('其他借阅者')
.fontSize(30)
.fontWeight(800)
ForEach(this.allBooks,(item:Book) => {
Reader({ book:item })
},(item: Book) => item.id.toString())
Button('添加一本书')
.onClick(() => {
this.allBooks.push(new Book("我的奋斗",1200))
})
Button('删除第一本书')
.onClick(() => {
if(this.allBooks.length > 0){
this.allBooks.shift()
}else{
console.log('图书馆没书了!!!')
}
})
// 改写第二层数据,需要class类被@Observed修饰才可被渲染监听
Button("所有书籍都被阅读完毕")
.onClick(() => {
this.allBooks.forEach((item:Book) => item.read = true)
})
} .width('100%')
.height('100%')
.justifyContent(FlexAlign.Center) }
}
//父组件中的@State数组项到@Prop class类型的同步
@Component
struct Reader{
@Prop book: Book = new Book('',0)
build() {
Row(){
Text(`${this.book ? this.book.title : '这本书不存在'}`)
.fontColor(Color.Red)
.fontSize(20)
Text(`有${this.book ? this.book.pages : '这本书不存在'}页`)
.fontColor(Color.Blue)
.fontSize(20)
Text(`${this.book ? this.book.read ? '已经读完了' : '没有读完' : '这本书不存在'}`)
.fontWeight(600)
.onClick(() => {
this.book.read = true
})
}
}
}
//在嵌套场景下,每一层都需要用@Observed装饰,且每一层都要被@Prop接受,这样才能观察嵌套场景
//修改第二层数据,父组件监听不到第二层数据,由于传递的是数据项,相当于子组件监听的是第一层的数据(相对于父组件是第二层)子组件可以遍历渲染
// 当第一层数据刷新时,会带动第二层的数据,所以第二层数据的变化,在父组件渲染出来
@Observed
class ClassA{
title: string
constructor(title: string) {
this.title = title
}
}
@Observed
class ClassB{
name:string
a:ClassA
constructor(name: string, a: ClassA) {
this.name = name
this.a = a
}
}
@Entry
@Component
/*
作者:ZhuWei
时间:2024/8/13
*/
struct mytest02 {
@State message: string = 'Hello World';
@State votes:ClassB = new ClassB('Hello',new ClassA('world'))
build() {
Column() {
//@Prop的嵌套场景
Button('修改ClassB name')
.onClick(() => {
this.votes.name = 'Hi'
})
Button('修改ClassA title')
.onClick(() => {
this.votes.a.title = 'ArkTs'
})
Text(`ClassB name:${this.votes.name}`)
.fontSize(30)
.onClick(() => {
this.votes.name = 'Bye'
})
Text(`ClassA title:${this.votes.a.title}`)
.fontSize(30)
.onClick(() => {
this.votes.a.title = 'openHarmony'
})
ChildA({ voteA: this.votes.a })
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
}
}
//@Prop嵌套场景
@Component
struct ChildA{
@Prop voteA: ClassA = new ClassA('')
build() {
Column(){
Text(`子组件ClassA title:${this.voteA.title}`)
.fontSize(30)
.onClick(() => {
this.voteA.title = 'Bye'
})
}
}
}
@Provide装饰的状态变量自动对其所有的后代组件可用,即该变量被"Provide"给他的后代组件,由此可见,@Provide方便之处在于不需要开发者多次在组件之间传递
后代通过使用@Consume去取@Provide提供的变量,建立在@Provide与@Consume之间的双向数据同步,与@State/@Link 不同的是,前者在多层次的父子组件之间传递
@Provide和@Consume可以通过相同的变量名或者相同的变量别名绑定, 建议类型相同,否则会发生类型隐式转换,从而导致应用行为异常
ps:toString( ) String( ) toFixed( ) 强制类型转换 console.log( number + ‘ ’ ) 隐式类型转换
/* 通过相同的变量名绑定
* 祖先组件: @Provide a:numer = 0
* 后代组件: @Consume a:number √
* 通过相同的变量别名绑定
* 祖先组件: @Provide('a') b:number = 0
* 后代组件: @Consume('a') c:number √
*
* 祖先组件: @Provide('a') b:number = 0
* 后代组件: @Consume('b') b:number ×
* @Provide和@Consume通过相同的变量名或者相同的变量别名
* 绑定时,@Provide装饰的变量和@Consume的变量是一对多
* 的关系。不允许出现在一个组件内,包括其子组件中声明的
* 多个同名或同别名的@Provide装饰的变量,
* @Provide的属性名或别名需要唯一且确定,如果声明多个同名或者
* 同别名的@Provide装饰的变量,会发生运行时报错
* 祖先组件 @Provide a:numer = 0 | @Provide('a') b:number = 0
* 后代组件 @Provide a:numer = 0 | @Provide('a') b:number 不允许
*/
/* @Provide
* @State的规则同样适用于@Provide,
* 差异为@Provide还作为多层后代的同步源
* @Provide拥有参数,别名:常量字符串,可选。如果指定了别名,则通过别名绑定;
* 如果未指定别名,则通过变量名绑定
* 同步类型为双向同步,从@Provide变量到所有@Consume变量以及相反方向的数据同步
*
* 允许装饰的变量类型: 同@State
*
* 类型与初始值:必须指定类型和初始值
*
*@Consume
*@Consume拥有参数:别名,如果提供了别名给@Consume,则必须有@Provide
* 变量拥有其形同的别名才能匹配成功;否则,则需要变量名相同才能匹配成功
* 同步类型:双向同步,从@Provide变量到所有的@Consume变量,以及相反的方法
* 双向同步的操作与@State和@Link相同
*
* 允许装饰的变量类型:同@State一致
*
* 类型与初始化:必须指定类型,建议与同名或同别名@Provide相同
* 禁止本地初始化
*
*
* @Provide从父组件初始化和更新 作为子组件
* 初始化可选,允许从父组件中的(常规变量.@Stata......)
* 用于初始化子组件 命名参数机制
* 可用于初始化@State、@Link、@Prop、@Provide
* 与父组件同步:无
* 和后代组件中的@Consume双向同步
*
* @Consume 不允许从父组件去初始化。
* 只能通过相同变量名或者相同变量别名从@Provide初始化
* 用于初始化其他子组件:允许,可以初始化@State、@Link、@Prop、@Provide
*
* 观察变化:与@State、@Prop、@Link一致
*/
类装饰器,装饰class,需要放在class定义,使用new创建对象
@ObjectLink修饰变量类型,必须被@Observed装饰的class实例,必须指定类型,不支持简单类型,如果想要装饰简单类型,使用@Prop
/*
* @Observed
* class Item{}
* @ObjectLink _object:Item
* 允许@ObjectLink装饰的变量属性赋值
* this._object.title = '....'
* 不允许@ObjectLink装饰的数据自身赋值
* this._object = new Item(....) ×
*/
/*
* @Watch('abc') 装饰器参数必填 常量字符串
* abc():void{}
* 是(string) => void 自定义成员函数的方法引用
* 可监听所有装饰器装饰的变量。不允许监听常规变量
*
* 写法:@State@Watch('a') a:number = 1
* @State@Watch('a') b:number = 2
* a('a','b'){}
*/
/* a(changePropertyName?:string) => void
* 该函数时自定义组件的成员函数,changePropertyName
* 是被Watch装饰的属性名。在多个状态变量绑定同一个@Watch
* 的回调方法的时候,可以changePropertyName进行不同的逻辑处理
* 将属性名作为字符串输入参数,不返回任何内容
*
* 开发者应关注性能,属性值的更新函数会延迟组件的重新
* 因此,回调函数仅执行快速运算
* 为了避免陷入无限循环。循环的原因可能是因为@Watch的回调方法里面
* 直接或者间接修改了同一个状态变量。为了避免循环的产生
* 建议不要再@Watch的回调方法里面修改当前装饰的状态变量
*
* */