基于状态变量实现复杂对象的状态监听
场景一:简单对象监听
对象在我们开发过程中是很常见的数据类型,我们在进行UI渲染的时候经常会用到对象,这里提供简单对象(所有字段均为基本数据类型)的监听效果。
方案一:状态管理V1实现
简单对象可以直接使用@State观测,这里使用起来比较简单,但是需要注意的是如果某个Class在页面中渲染中使用的字段较多的时候,可以结合@Track修饰器来进行观测。
实现代码:
@Builder
export function SimpleObjectBuilder(name: string, param: Object) {
SimpleObject()
}
class LogTrack {
// 这里使用@Track可以达到最小化更新的效果
@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 = '世界';
}
}
@Component
export struct SimpleObject {
pathStack: NavPathStack = new NavPathStack()
@State logTrack: LogTrack = new LogTrack('Hello');
@State logNotTrack: LogNotTrack = new LogNotTrack('你好');
isRender(index: number) {
console.log(`Text ${index} is rendered`);
return 50;
}
build() {
NavDestination() {
Column() {
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(() => {
// 点击更新str1 字段,观察页面日志变化,会发现使用@Track修饰的变量只会对改变的变量做最小化更新
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(() => {
// 点击更新str1字段,观察页面日志变化,会发现在不使用@Track修饰即使只改变了一个值,但是对象中所有的字段全都会触发UI更新那么值没有发生改变
this.logNotTrack.str1 = '再见';
})
}
}.width('100%').height('100%')
}.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack;
})
}
}
实现效果如下:
观察日志发现第一种在更新UI的时候只会对操作的UI进行重绘渲染,而第二种在更新UI的时候所有在页面中用到了的组件都会进行重绘,所以为了解释性能,我们更建议在Class中使用@Track对字段进行精准修饰。
方案二:状态管理V2实现
在状态管理V2中对于对象的观察只有一套框架@ObservedV2+@Trace,它们具备深度观测对象的能力,简单对象也同样可以观测。
需要注意的是使用@ObservedV2修饰的类,如果字段需要观测变化的时候需要对该字段使用@Trace进行修饰,否则UI不会刷新。
实现代码:
@Builder
export function SimpleObjectBuilderV2(name: string, param: Object) {
SimpleObject()
}
@ObservedV2
class LogTrack {
@Trace str1: string;
str2: string;
constructor(str1: string) {
this.str1 = str1;
this.str2 = 'World';
}
}
@ComponentV2
struct SimpleObject {
pathStack: NavPathStack = new NavPathStack()
logTrack: LogTrack = new LogTrack('hello');
isRender(index: number) {
console.log(`Text ${index} is rendered`);
return 50;
}
build() {
NavDestination() {
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(() => {
// 点击更新Str1和str2,str1使用了@Trace修饰,UI界面正常刷新,Str2没有修饰,UI界面不刷新
this.logTrack.str1 = 'Hi';
this.logTrack.str2 = '世界';
})
}
}.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack;
})
}
}
并且使用@Trace修饰的字段也具备最小化更新的能力,具体见下方日志:
另外还需要注意的是,被@ObservedV2与@Trace装饰的类对象实例,虽然具有深度观测对象属性的能力。但当对对象整体赋值时,UI却无法刷新。使用@Local装饰对象,可以达到观测对象本身变化的效果。
实现代码:
@ObservedV2
class Info {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
@Entry
@ComponentV2
struct Index {
info1: Info = new Info('Tom', 25);
@Local info2: Info = new Info('Tom', 25);
build() {
Column() {
Text(`info1: ${this.info1.name}-${this.info1.age}`) // Text1
Text(`info2: ${this.info2.name}-${this.info2.age}`) // Text2
Button('change info1&info2')
.onClick(() => {
this.info1 = new Info('Lucy', 18); // Text1不会刷新
this.info2 = new Info('Lucy', 18); // Text2会刷新
})
}
}
}
场景二:复杂对象监听
复杂对象一般是指在对象字段中还存在其他对象(嵌套对象)的场景。
方案一:状态管理V1实现
@State装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套对象的情况,它们的第二层的属性变化是无法观察到的。我们先看数据模型类图:
在上面数据模型中,Class User的字段bag是Class Bag类型,Class Book的字段bookName是Class BookName类型,这种情况都是一个对象的字段中还含有一个对象,这一类数据我们叫复杂对象,这类数据在使用状态变量监听的时候会比较麻烦,@State/@Prop等修饰器只能监听一层,也就是对象下的基本数据类型字段,对于多次嵌套的对象,在深度监听的时候需要用@Observed/@ObjectLink装饰器来进行深度观测,请看下面实现;总结下面代码,我们不难发现在状态管理V1中一层组件永远只能监听一层对象,如果涉及到多层对象,我们就需要分子组件来进行监听同步。
@Builder
export function ComplexObjectBuilderV1(name: string, param: Object) {
ComplexObject()
}
// objectLinkNestedObjects.ets
let NextID: number = 1;
// 这里的对象我们都需要深度监听,Class类先使用@Observed修饰
@Observed
class Bag {
public id: number;
public size: number;
constructor(size: number) {
this.id = NextID++;
this.size = size;
}
}
@Observed
class User {
public bag: Bag;
constructor(bag: Bag) {
this.bag = bag;
}
}
@Observed
class Book {
public bookName: BookName;
constructor(bookName: BookName) {
this.bookName = bookName;
}
}
@Observed
class BookName extends Bag {
public nameSize: number;
constructor(nameSize: number) {
// 调用父类方法对nameSize进行处理
super(nameSize);
this.nameSize = nameSize;
}
}
@Component
export struct ComplexObject {
pathStack: NavPathStack = new NavPathStack()
// 初始化状态变量,因为@ObjectLink修饰器的入参类型必须也是状态变量,所以我们第一层使用@State修饰
@State user: User = new User(new Bag(0));
@State child: Book = new Book(new BookName(0));
build() {
NavDestination() {
Column() {
// @ObjectLink是子组件与父组件双向同步,对于这类复杂对象无法做到在一个组件中实现深度监听,需要分层级来实现,嵌套多少层就需要分多少层组件
ViewA({ label: 'ViewA #1', bag: this.user.bag })
.width(320)
ViewC({ label: 'ViewC #3', bookName: this.child.bookName })
.width(320)
Button(`ViewC: this.child.bookName.size add 10`)
.width(320)
.backgroundColor('#ff17a98d')
.margin(10)
.onClick(() => {
// 使用@State修饰并且子组件使用了@ObjectLink修饰,因此能达到父组件状态子组件同步的效果,在父组件更新可以生效,如果子组件没有使用@ObjectLink修饰,则不生效
this.child.bookName.size += 10
console.log('this.child.bookName.size:' + this.child.bookName.size)
})
Button(`ViewB: this.user.bag = new Bag(10)`)
.width(320)
.backgroundColor('#ff17a98d')
.margin(10)
.onClick(() => {
// 直接替换user.bag,此时相当于是bag是整个值替换,因为@State能够监听一层,所以UI会改变,这里的原理与简单对象监听原理一致
this.user.bag = new Bag(10);
})
Button(`ViewB: this.user = new User(new Bag(20))`)
.width(320)
.backgroundColor('#ff17a98d')
.margin(10)
.onClick(() => {
// 直接替换整个user使用@State能够监听到user的变化,这里就相当于基本数据类型使用@State监听原理一致
this.user = new User(new Bag(20));
})
}.width('100%').height('100%')
}.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack;
})
}
}
@Component
struct ViewA {
label: string = 'ViewA';
// 接收父级对象的Bag对象
@ObjectLink bag: Bag;
build() {
Column() {
Text(`ViewC [${this.label}] this.bag.size = ${this.bag.size}`)
.fontColor('#ffffffff')
.backgroundColor('#ff3d9dba')
.width(320)
.height(50)
.margin(10)
.textAlign(TextAlign.Center)
Button(`ViewA: this.bag.size add 1`)
.width(320)
.backgroundColor('#ff17a98d')
.margin(10)
.onClick(() => {
// 更新子组件bag的字段,因为使用了@ObjeckLink修饰父组件传入的对象,可以达到父子组件同步的效果,因此UI会刷新
this.bag.size += 1;
})
}
}
}
@Component
struct ViewC {
label: string = 'ViewC1';
@ObjectLink bookName: BookName;
build() {
Row() {
Column() {
Text(`ViewC [${this.label}] this.bookName.size = ${this.bookName.size}`)
.fontColor('#ffffffff')
.backgroundColor('#ff3d9dba')
.width(320)
.height(50)
.margin(10)
.textAlign(TextAlign.Center)
Button(`ViewC: this.bookName.size add 1`)
.width(320)
.backgroundColor('#ff17a98d')
.margin(10)
.onClick(() => {
// 与上面同理,因为使用了@ObjectLink修饰,能达到父子组件同步的效果,因此子组件状态变化UI会刷新
this.bookName.size += 1;
console.log('this.bookName.size:' + this.bookName.size)
})
}
.width(320)
}
}
}
实现效果图如下:
方案二:状态管理V2实现
同样场景下,类图见状态管理V1,在状态管理V2中实现起来就要方便的多,我们在场景一中介绍了,状态管理V2对于对象的监听只有@ObservedV2+@Trace修饰器,复杂对象我们也是使用这个修饰器,具体实现见下面代码。
在使用状态管理V2进行观测的时候,我们无须考虑当前对象是否是多层嵌套对象,需要需要分子组件来进行观测,只需要关注Class中的字段是否需要展示在UI中即可。
@Builder
export function ComplexObjectBuilderV2(name: string, param: Object) {
ComplexObject()
}
let NextID: number = 1;
@ObservedV2
class Bag {
public id: number;
// 在UI中展示的字段需要使用@Trace修饰,否则无法观测,UI无法刷线
@Trace public size: number;
constructor(size: number) {
this.id = NextID++;
this.size = size;
}
}
@ObservedV2
class User {
@Trace public bag: Bag;
constructor(bag: Bag) {
this.bag = bag;
}
}
@ObservedV2
class Book {
@Trace public bookName: BookName;
constructor(bookName: BookName) {
this.bookName = bookName;
}
}
@ObservedV2
class BookName extends Bag {
public nameSize: number;
constructor(nameSize: number) {
// 调用父类方法对nameSize进行处理
super(nameSize);
this.nameSize = nameSize;
}
}
@ComponentV2
export struct ComplexObject {
pathStack: NavPathStack = new NavPathStack()
// 初始化状态变量,在简单对象修饰中讲解过了,@ObservedV2+@Trace能实现对象的深度观测,但是当自身被替换的时候无法观测到,我们需要使用@Local观测
@Local user: User = new User(new Bag(0));
@Local child: Book = new Book(new BookName(0));
build() {
NavDestination() {
Column() {
Column() {
Text(`this.user.bag.size = ${this.user.bag.size}`)
.fontColor('#ffffffff')
.backgroundColor('#ff3d9dba')
.width(320)
.height(50)
.margin(10)
.textAlign(TextAlign.Center)
Button(`ViewA: this.bag.size add 1`)
.width(320)
.backgroundColor('#ff17a98d')
.margin(10)
.onClick(() => {
// @Trace修饰的字段具备深度观测能力,可以直接修改
this.user.bag.size += 1;
})
}.width(320)
Column() {
Text(`this.child.bookName.size = ${this.child.bookName.size}`)
.fontColor('#ffffffff')
.backgroundColor('#ff3d9dba')
.width(320)
.height(50)
.margin(10)
.textAlign(TextAlign.Center)
Button(`ViewC: this.bookName.size add 1`)
.width(320)
.backgroundColor('#ff17a98d')
.margin(10)
.onClick(() => {
// @Trace修饰的字段具备深度观测能力,可以直接修改
this.child.bookName.size += 1;
console.log('this.bookName.size:' + this.child.bookName.size)
})
}.width(320)
Button(`ViewC: this.child.bookName.size add 10`)
.width(320)
.backgroundColor('#ff17a98d')
.margin(10)
.onClick(() => {
// @Trace修饰的字段具备深度观测能力,可以直接修改
this.child.bookName.size += 10
console.log('this.child.bookName.size:' + this.child.bookName.size)
})
Button(`ViewB: this.user.bag = new Bag(10)`)
.width(320)
.backgroundColor('#ff17a98d')
.margin(10)
.onClick(() => {
// @Trace修饰的字段具备深度观测能力,可以直接修改
this.user.bag = new Bag(10);
})
Button(`ViewB: this.user = new User(new Bag(20))`)
.width(320)
.backgroundColor('#ff17a98d')
.margin(10)
.onClick(() => {
// @Trace修饰的字段具备深度观测能力,可以直接修改
this.user = new User(new Bag(20));
})
}.width('100%').height('100%')
}.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack;
})
}
}
实现效果与状态管理V1一致。
场景三:对象数组监听
对象数组是指该数组的每一项元素都是对象的场景,该数据类型属于我们开发中最场景的一种场景,下面实现一个双层对象数据的场景示例,具体数据模型见下图。
AllList{}-->Outer[]--->Inner[]
方案一:状态管理V1实现
在场景二中我们介绍了,状态管理V1在一层组件中只能监听对象下面的一层字段,因此这里需要分多层子组件来实现,具体结构是与数据模型保持一致,组件关系与数据模型对应图示如下:
数据分层代码如下:
前面介绍了,因为无法观测多层对象的变化,因此我们只能将数据拆分成一层一层的来观测。
// model.ets
/**
* 列表数据结构模型
*/
export class AllList {
// 列表总数据源
items: Outer[] = []
// 内层列表勾选状态下输入总金额
allAmount: string = ''
constructor(items: Outer[], allAmount: string) {
this.items = items
this.allAmount = allAmount
}
}
@Observed
export class Outer {
// 外层label 类似于视频中的居民、企事业
public label: string = ''
//外层label展开状态,点击显示里层inners,再次点击隐藏inners
public expand: boolean = false
//内部数据源 类似视频中的每个用点户号
public inners: Inner[] = []
constructor(label: string, inners: Inner[], expand?: boolean) {
this.label = label
this.inners = inners
this.expand = expand ? expand : false
}
}
@Observed
export class Inner {
public id: string
// 户号名称
public innerName: string = ''
// 当前户号的勾选状态
public checkStatus: boolean = false
// 推荐金额 类似于视频重的按钮金额 ¥20/¥50/¥100/¥200
public btnMoney: string = '0'
// 输入框输入的金额 只能是数字、两位小数
public inputContent: string = ''
constructor(innerName: string) {
this.innerName = innerName
this.id = Math.random().toString()
}
}
UI实现
然后在页面中使用的时候也是一层组件对应一层数据结构。
// index.ets
import { Inner, Outer } from '../model/Model'
import { OuterComponent } from './components/ListComponent'
// 创建新实例并使用给定对象初始化
let para: Record<string, number> = { 'allAmount': 0 };
let storage: LocalStorage = new LocalStorage(para);
@Entry(storage)
@Component
struct Index {
// 首页Index对应AllList,这里没有直接使用上面的AllList对象,而是直接使用状态变量将两个字段分别观测了
@Provide AllList: Outer[] = []
@LocalStorageLink('allAmount') allAmount: number = 0
aboutToAppear(): void {
// 初始化数据
this.AllList = [
new Outer('居民', [new Inner('光谷理想城x栋x单元xxx'), new Inner('保利时代x栋x单元xxx'), new Inner('南湖名都x栋x单元xxx')]),
new Outer('企事业', [new Inner('光谷软件园x栋x楼xxxx'), new Inner('光谷金融港x栋x楼xxx'), new Inner('光谷智慧园x栋x楼xxx')])
]
}
build() {
Column() {
...
List() {
ForEach(this.AllList, (outer: Outer) => {
if (outer.inners.length) {
// 遍历数组将对应的Outer对象传递给OuterComponent子组件,在子组件完成Outer对象的观测并同步给父组件
OuterComponent({ outer: outer })
}
}, (item: Outer) => item.label)
}
...
}.backgroundColor('#e9ecef').width('100%').height('100%')
}
}
// ListComponent.ets
import { Inner, Outer } from '../../model/Model'
@Component
export struct OuterComponent {
// 接收Index传进来的Outer对象,并使用@ObjectLink观测实现父子组件双向同步
@ObjectLink outer: Outer
@State arrowImg: Resource = $r('sys.media.ohos_ic_public_arrow_up')
...
// 只展示核心代码,完整demo见最后附件
build() {
ListItemGroup({ header: this.itemHead(this.outer.label), style: ListItemGroupStyle.NONE }) {
ForEach(this.outer.inners, (inner: Inner, index: number) => {
ListItem({ style: ListItemStyle.NONE }) {
// 因为当前的Outer对象中还存在Inner对象数组,因此继续分层
InnerComponent({ inner: inner })
}
}, (inner: Inner) => inner.innerName)
}.backgroundColor('#e9ecef')
.margin({ top: 16 })
}
}
@Component
struct InnerComponent {
// 接受OuterComponent传进来的Inner对象,并使用@ObjectLink观测实现父子组件双向同步
@ObjectLink inner: Inner
build() {
...
// 只展示核心代码,完整demo见最后附件
}
}
方案二:状态管理V2实现
这种方式对比方案一,无需对组件分层,只用在需要UI展示的字段上使用@Trace修饰器修饰即可,具体实现如下:
/*
列表数据结构模型
*/
@ObservedV2
class AllList {
// 列表总数据源
@Trace items: Outer[] = []
// 内层列表勾选状态下输入总金额
@Trace allAmount: string = ''
constructor(items: Outer[], allAmount: string) {
this.items = items
this.allAmount = allAmount
}
}
@ObservedV2
class Outer {
// 外层label 类似于视频中的居民、企事业
@Trace label: string = ''
//外层label展开状态,点击显示里层inners,再次点击隐藏inners
@Trace expand: boolean = false
//内部数据源 类似视频中的每个用点户号
@Trace inners: Inner[] = []
constructor(label: string, inners: Inner[], expand?: boolean) {
this.label = label
this.inners = inners
this.expand = expand ? expand : false
}
}
@ObservedV2
class Inner {
@Trace id: string
// 户号名称
@Trace innerName: string = ''
// 当前户号的勾选状态
@Trace checkStatus: boolean = false
// 推荐金额 类似于视频重的按钮金额 ¥20/¥50/¥100/¥200
@Trace btnMoney: string = '0'
// 输入框输入的金额 只能是数字、两位小数
@Trace inputContent: string = ''
constructor(innerName: string) {
this.innerName = innerName
this.id = Math.random().toString()
}
}
@Entry
@Component
struct IndexV2 {
// 数据初始化
AllList: AllList = new AllList([
new Outer('居民',
[new Inner('xx小区x栋x单元xxx'), new Inner('xx小区x栋x单元xxx'), new Inner('xx小区x栋x单元xxx')]),
new Outer('企事业',
[new Inner('xx小区x栋x单元xxx'), new Inner('xx小区栋x单元xxx'), new Inner('xx小区x栋x单元xxx')])
], '0')
// ...
// 只展示数据监听核心代码,详细实现请看文章结尾附件
build() {
Column() {
List() {
// 因为@ObservedV2具备深入观测能力,使用时直接使用数据对象往下.即可
ForEach(this.AllList.items, (outer: Outer) => {
ListItemGroup({ header: this.itemHead(outer), style: ListItemGroupStyle.NONE }) {
ForEach(outer.inners, (inner: Inner, index: number) => {
ListItem({ style: ListItemStyle.NONE }) {
// ...
// 只展示数据监听核心代码,详细实现请看文章结尾附件
}
}, (inner: Inner) => inner.innerName)
}.backgroundColor('#e9ecef')
.margin({ top: 16 })
}, (item: Outer) => item.label)
}
.width('90%').height('100%')
}.backgroundColor('#e9ecef').width('100%').height('100%')
}
}
汇总对比
场景 | 状态管理V1 | 状态管理V2 |
简单对象 | 使用@State观测,在字段过多时建议结合@Track做最小化观测 | 使用@ObservedV2+@Trace观测,需要UI展示的字段使用@Trace修饰,同时如果涉及到整个对象替换的时候需要在组件中使用@Local修饰 |
复杂对象 | 使用@Observed+@ObjectLink观测,但是对应多层数据需要数据分层的同时还需要组件分层,通过这种方式虽然能够实现对嵌套类中属性变化的观测,但是当嵌套层级较深时,代码将会变得十分复杂,易用性差 | @ObservedV2装饰器与@Trace装饰器用于装饰类以及类中的属性,使得被装饰的类和属性具有深度观测的能力,对比V1的实现更符合开发逻辑,易用性与代码可读性都更好 |
以上是针对上面三个场景对V1与V2做到一个对比,下面针对V1与V2的各类装饰器做一个完整对比:
状态管理V1 | 状态管理V2 | 对比 | |
组件内状态 | @State | @Local | 状态管理V1使用@State定义类中的状态变量,@State装饰器能够从外部初始化,因此@State无法准确表达组件内部状态不能被外面修改的语义,而状态管理V2中的@Local无法从外传入初始化,因此能更准确的表达组件的内部状态 |
组件外部输入 | @State/@Prop/@Link/@ObjectLink | @Param | 状态管理V1存在多种可接受外部传入的装饰器,常用的有@State、@Prop、@Link、@ObjectLink。这些装饰器使用各有限制,不易区分,当使用不当时,还会导致性能问题。而状态管理V2只有@Param装饰器表示组件从外部传入的状态,并且可以实现与父组件@Local修饰的变量进行同步 |
状态变量修改监听 | @Watch | @Monitor | @Watch无法实现对对象、数组中某一单个属性或数组项变化的监听,且无法获取变化之前的值。@Monitor装饰器实现对对象、数组中某一单个属性或数组项变化的监听,并且能够获取到变化之前的值 |
更多资料,请参考文档:状态管理V2。